Android文件挑选器的完结

前语

此项目和之前发布的项目有些不同,之前都是根本的功用,不是基于 Activity 页面完结的,而相似文件挑选,图片挑选,除了功用的完结还需求处理 UI 相关的装备。

在前面的【怎么操作文件的挑选】 一文中我就想把逻辑做一下封装,做成开箱即用的文件挑选器,本来这功用是项目中自用的,UI 等都是自有的,假如要做开源出去,那么就要抽取功用与 UI 逻辑,设置可装备选项。

分化一下完结步骤,怎么自界说一个文件下载器呢?

  1. 咱们需求装备 Activity 根本的 Theme,动画,状况栏,导航栏等处理。
  2. 咱们需求装备展现的文本巨细,回来图标,列表与导航栏的文本巨细等等。
  3. 然后咱们对XML的布局并构建导航列表与文件列表的数据适配器等。
  4. 然后咱们就能够处理权限以及对文件的操作了。
  5. 能够运用战略形式不同的版别不同的完结办法。
  6. 过滤操作是比不可少的,咱们获取文件之后运用过滤操作展现咱们想要的文件。

这样差不多就能完结一个根本的操作文件挑选结构了。

这儿先放完结之后各版别手机的截图,详细作用如下:

Android 7.0 作用(华为):

从零开始实现一个兼容版本的Android文件选择器

Android 9.0 作用(谷歌):

从零开始实现一个兼容版本的Android文件选择器

Android 12 作用(三星):

从零开始实现一个兼容版本的Android文件选择器

Android 13 作用(Vivo):

从零开始实现一个兼容版本的Android文件选择器

结构完结基于 target 31,不装备兼容形式 requestLegacyExternalStorage ,支撑 4.4 及以上系统,可保持UI的一致性…

话不多说,赶忙开始吧!

从零开始实现一个兼容版本的Android文件选择器

一、文件挑选的页面的装备

咱们运用咱们自界说的theme与动画即可。因为咱们要自己完结可控的标题栏,所以咱们的样式不需求toolbar:

<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/choose_file_app_blue</item>
        <item name="colorPrimaryDark">@color/choose_file_app_blue</item>
        <item name="colorAccent">@color/choose_file_app_blue</item>
        <item name="android:windowAnimationStyle">@style/My_AnimationActivity</item>
        <item name="android:windowIsTranslucent">false</item>
    </style>
    <style name="My_AnimationActivity" mce_bogus="1" parent="@android:style/Animation.Activity">
        <item name="android:activityOpenEnterAnimation">@anim/open_enter</item>
        <item name="android:activityCloseExitAnimation">@anim/close_exit</item>
    </style>
</resources>

为了可装备的状况栏与导航栏,这儿我用到之前的项目中的 StatusBarHost 结构,详细的完结与细节能够查看之前的文章,【传送门】。

那么咱们创立挑选文件的Activity大致如下:

class ChooseFileActivity : AppCompatActivity(), View.OnClickListener {
    private val mViewModel: ChooseFileViewModel by lazy {
        ViewModelProvider(this, ChooseFileViewModelFactory()).get(ChooseFileViewModel::class.java)
    }
    private var mainHandler = Handler(Looper.getMainLooper())
    //展现当时页面的UI风格
    private val uiConfig = ChooseFile.config?.mUIConfig ?: ChooseFileUIConfig.Builder().build()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_choose_file)
        StatusBarHost.inject(this)
            .setStatusBarBackground(uiConfig.statusBarColor)
            .setStatusBarWhiteText()
            .setNavigationBarBackground(uiConfig.navigationBarColor)
            .setNavigatiopnBarIconBlack()
    }

而为了横竖屏切换的作用,或许说为了适配折叠屏设备,咱们能够运用ViewModel保存一些页面状况:

class ChooseFileViewModel : ViewModel() {
    val mNavPathList = arrayListOf<ChooseFileInfo>()
    var mNavAdapter: FileNavAdapter? = null
    val mFileList = arrayListOf<ChooseFileInfo>()
    var mFileListAdapter: FileListAdapter? = null
    //根目录
    val rootPath = Environment.getExternalStorageDirectory().absolutePath
    var rootChoosePos = 0  //根目录文档选中的索引
    //当时挑选的途径
    var mCurPath = Environment.getExternalStorageDirectory().absolutePath
}

这儿现已用到了一些UI的装备选项,咱们赶忙接下来往下走。

二、页面的UI装备与其他装备

一般咱们都会依据不同的UI作用,设置不同的文本色彩和布景,所以咱们需求把页面上的文本与布景和图标等选项抽取出来,装备成可选的特点:

public class ChooseFileUIConfig {
    private int statusBarColor;   //状况栏色彩
    private int titleBarBgColor;  //标题栏的布景色彩
    private int titleBarBackRes;  //标题栏的回来按钮资源
    private int titleBarTitleColor; //标题栏的标题文字色彩
    private int titleBarTitleSize; //标题栏的标题文字巨细(sp)
    private int navigationBarColor; //底部导航栏色彩
    private int fileNavBarTextColor; //文件导航栏的文本色彩
    private int fileNavBarTextSize; //文件导航栏的文本巨细
    private int fileNavBarArrowIconRes; //文件导航栏的箭头图标资源
    private int fileNameTextColor;  //文件(夹)称号字体色彩
    private int fileNameTextSize;  //文件(夹)称号字体巨细(sp)
    private int fileInfoTextColor;  //文件(夹)提示信息字体巨细
    private int fileInfoTextSize;  //文件(夹)提示信息字体巨细(sp)
    private ChooseFileUIConfig() {
    }
    ...
}

然后咱们运用构建者形式创立可选的装备,假如不挑选那么就能够运用默许的装备,就特别合适此场景:

  public static class Builder {
        private int statusBarColor = Color.parseColor("#0689FB");   //状况栏色彩
        private int titleBarBgColor = Color.parseColor("#0689FB");  //标题栏的布景色彩
        private int titleBarBackRes = R.drawable.cf_back;  //标题栏的回来按钮资源
        private int titleBarTitleColor = Color.parseColor("#FFFFFF"); //标题栏的标题文字色彩
        private int titleBarTitleSize = 20; //标题栏的标题文字巨细(sp)
        private int navigationBarColor = Color.parseColor("#F7F7FB"); //底部导航栏色彩
        private int fileNavBarTextColor = Color.parseColor("#333333"); //文件导航栏的文本色彩
        private int fileNavBarTextSize = 15; //文件导航栏的文本巨细
        private int fileNavBarArrowIconRes = R.drawable.cf_next; //文件导航栏的箭头图标资源
        private int fileNameTextColor = Color.BLACK;  //文件(夹)称号字体色彩
        private int fileNameTextSize = 16;  //文件(夹)称号字体巨细(sp)
        private int fileInfoTextColor = Color.parseColor("#A9A9A9");  //文件(夹)提示信息字体巨细
        private int fileInfoTextSize = 14;  //文件(夹)提示信息字体巨细(sp)
        public Builder() {
        }
        public Builder statusBarColor(int statusBarColor) {
            this.statusBarColor = statusBarColor;
            return this;
        }
     ...   

UI的装备完结之后,咱们还需求对一些常规的装备做一些可选操作,例如线程池的自界说,过滤文件的挑选等等。

class ChooseFileConfig(private val chooseFile: ChooseFile) {
    internal var mUIConfig: ChooseFileUIConfig? = null
    internal var mIFileTypeFilter: IFileTypeFilter? = null
    internal var mExecutor: ExecutorService? = ThreadPoolExecutor(
        1, 1, 10L, TimeUnit.MINUTES, LinkedBlockingDeque()
    )
    fun setUIConfig(uiConfig: ChooseFileUIConfig?): ChooseFileConfig {
        mUIConfig = uiConfig
        return this
    }
    fun setExecutor(executor: ExecutorService): ChooseFileConfig {
        mExecutor = executor
        return this
    }
    fun getExecutor(): ExecutorService? {
        return mExecutor
    }
    fun setTypeFilter(filter: IFileTypeFilter): ChooseFileConfig {
        mIFileTypeFilter = filter
        return this
    }
    fun forResult(listener: IFileChooseListener) {
        val activity = chooseFile.activityRef?.get()
        activity?.gotoActivityForResult<ChooseFileActivity> {
            it?.run {
                val info = getSerializableExtra("chooseFile") as ChooseFileInfo
                listener.doChoose(info)
            }
        }
    }
    //毁掉资源
    fun clear() {
        mUIConfig = null
        mIFileTypeFilter = null
        if (mExecutor != null && !mExecutor!!.isShutdown) {
            mExecutor!!.shutdown()
        }
    }
}

因为操作文件是耗时的操作,咱们最好是在线程中进行,咱们一致运用默许的线程池处理,假如用户想自界说运用能够他自己的线程池。

而 forResult 的完结咱们是对 startActivityForResult 的封装,为了兼容低版别内部是 Ghost 完结。

而内部运用到的 ChooseFile 则是咱们的单例运用进口,内部完结如下:

object ChooseFile {
    @JvmField
    internal var activityRef: WeakReference<FragmentActivity>? = null
    @JvmField
    internal var config: ChooseFileConfig? = null
    @JvmStatic
    fun create(activity: FragmentActivity): ChooseFileConfig {
        activityRef?.clear()
        this.activityRef = WeakReference(activity)
        config = ChooseFileConfig(this)
        return config!!
    }
    @JvmStatic
    fun create(fragment: Fragment): ChooseFileConfig {
        activityRef?.clear()
        val activity = fragment.requireActivity()
        this.activityRef = WeakReference(activity)
        config = ChooseFileConfig(this)
        return config!!
    }
    @JvmStatic
    fun release() {
        activityRef?.clear()
        config?.clear()
        config = null
    }
}

到处咱们就能够正常的运用结构了:


        findViewById<Button>(R.id.btn_get_file).setOnClickListener {
            ChooseFile.create(this)
                .setUIConfig(ChooseFileUIConfig.Builder().build())
                .setTypeFilter { listData ->
                    return@setTypeFilter ArrayList(listData.filter { item ->
                        //只需文件夹
                          item.isDir
                        //只需文档文件
//                        item.fileType == ChooseFile.FILE_TYPE_FOLDER ||
//                                item.fileType == ChooseFile.FILE_TYPE_TEXT ||
//                                item.fileType == ChooseFile.FILE_TYPE_PDF
                    })
                }
                .forResult {
                    Toast.makeText(this, "选中的文件:" + it?.fileName, Toast.LENGTH_SHORT).show()
                    val uri = Uri.parse(it?.filePathUri)
                    val fis = contentResolver.openInputStream(uri)
                    Log.w("TAG", "文件的Uri:" + it?.filePathUri + " uri:" + uri + " fis:" + fis)
                    fis?.close()
                }
        }

这样拉到列表的底部之后就只会显现文件夹类型:

从零开始实现一个兼容版本的Android文件选择器

三、导航列表与文件列表的展现

对应文件列表的展现以及文件导航的展现,咱们需求先界说对应的xml:

代码咱们都会,作用如下图:

从零开始实现一个兼容版本的Android文件选择器

那么RV的处理如下:

    private fun initRV() {
        mViewModel.mNavAdapter = FileNavAdapter(mViewModel.mNavPathList, uiConfig)
        rvNav.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
        rvNav.adapter = mViewModel.mNavAdapter
        mViewModel.mNavAdapter?.setOnNavClickListener { position ->
            val item = mViewModel.mNavPathList[position]
            mViewModel.mCurPath = item.filePath
            startRefreshAnim()
            obtainByPath(mViewModel.mCurPath)
        }
        mViewModel.mFileListAdapter = FileListAdapter(mViewModel.mFileList, uiConfig)
        rvFiles.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        rvFiles.adapter = mViewModel.mFileListAdapter
        mViewModel.mFileListAdapter?.setOnFileListClickListener() { position ->
            val item = mViewModel.mFileList[position]
            if (item.isDir) {
                //设置当时Root的选中
                if (mViewModel.mNavPathList.isEmpty()) {
                    mViewModel.rootChoosePos = (rvFiles.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
                }
                //文件夹-直接改写页面
                mViewModel.mCurPath = item.filePath
                startRefreshAnim()
                obtainByPath(mViewModel.mCurPath)
            } else {
                //选中文件-回调出去
                setResult(-1, Intent().putExtra("chooseFile", item))
                finish()
            }
        }
    }

Adapter的处理也很简略,咱们把UI的装备挑选传进来,然后做赋值操作即可,咱们最好是只做赋值操作,处理的逻辑都在文件的处理那一边处理,那儿是有子线程一并处理的。

class FileNavAdapter(private val navPathList: MutableList<ChooseFileInfo>, private val uiConfig: ChooseFileUIConfig) :
    RecyclerView.Adapter<FileNavAdapter.FileNavViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileNavViewHolder {
        val itemView = View.inflate(parent.context, R.layout.item_choose_file_nav, null)
        return FileNavViewHolder(itemView)
    }
    override fun onBindViewHolder(holder: FileNavViewHolder, position: Int) {
        holder.curPosition = position
        holder.tvPath.text = navPathList[position].fileName
        holder.tvPath.setTextColor(uiConfig.fileNavBarTextColor)
        holder.tvPath.setTextSize(TypedValue.COMPLEX_UNIT_SP, uiConfig.fileNameTextSize.toFloat())
        holder.ivPathSegment.setImageResource(uiConfig.fileNavBarArrowIconRes)
        if (position == (itemCount - 1)) holder.ivPathSegment.visibility = View.INVISIBLE
        else holder.ivPathSegment.visibility = View.VISIBLE
    }
    override fun getItemCount(): Int = navPathList.size
    inner class FileNavViewHolder(private val itemView: View) : ViewHolder(itemView) {
        val tvPath: TextView = itemView.findViewById(R.id.tv_root)
        val ivPathSegment: ImageView = itemView.findViewById(R.id.iv_path_segment)
        var curPosition: Int = 0
        init {
            itemView.setOnClickListener {
                mListener?.onClick(curPosition)
            }
        }
    }
    private var mListener: OnNavClickListener? = null
    fun setOnNavClickListener(listener: OnNavClickListener) {
        mListener = listener
    }
    fun interface OnNavClickListener {
        fun onClick(position: Int)
    }
}

两个 Adapter 的完结作用是相似的,就不多贴代码,有爱好能够去文章末尾找源码。

关于展现的Item的Bean目标,咱们需求运用自界说的 File 封装,作为展现的选项。咱们需求对文件进行读取之后直接封装到这个 Bean 目标中,方便直接展现。

public class ChooseFileInfo implements Serializable {
    public String fileName;
    public boolean isDir;  //是否是文件夹
    public String fileSize; //假如是文件夹则表明子目录项数,假如不是文件夹则表明文件巨细,当值为-1的时分不显现
    public String fileLastUpdateTime;   //最终操作事情
    public String filePath;             //文件的途径
    public String filePathUri;          //文件的途径,URI形式
    public String fileType;           //文件类型
    public int fileTypeIconRes;         //文件类型对应的图标展现
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ChooseFileInfo that = (ChooseFileInfo) o;
        return Objects.equals(filePath, that.filePath);
    }
    @Override
    public int hashCode() {
        return Objects.hash(filePath);
    }
}

需求留意的是咱们需求处理文件夹的选中与顶部文件导航的交互,两个RV选中之后需求有数据的逻辑处理。

底部的 RV 选中文件夹之后需求给顶部的文件导航增加数据,而顶部的文件导航选中之后需求改写底部的 RV 选中:

底部 RV 的选中:

     mViewModel.mFileListAdapter?.setOnFileListClickListener() { position ->
            val item = mViewModel.mFileList[position]
            if (item.isDir) {
                //设置当时Root的选中
                if (mViewModel.mNavPathList.isEmpty()) {
                    mViewModel.rootChoosePos = (rvFiles.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
                }
                //文件夹-直接改写页面
                mViewModel.mCurPath = item.filePath
                startRefreshAnim()
                obtainByPath(mViewModel.mCurPath)
            } else {
                //选中文件-回调出去
                setResult(-1, Intent().putExtra("chooseFile", item))
                finish()
            }
        }

顶部 RV 的选中:

 mViewModel.mNavAdapter?.setOnNavClickListener { position ->
            val item = mViewModel.mNavPathList[position]
            mViewModel.mCurPath = item.filePath
            startRefreshAnim()
            obtainByPath(mViewModel.mCurPath)
        }

加载数据完结之后的顶部导航展现逻辑:

  //顶部文件导航的设置
    private fun setTopNavSelect(topInfo: ChooseFileInfo?) {
        if (topInfo != null) {
            if (mViewModel.mNavPathList.isEmpty()) {
                mViewModel.mNavPathList.add(topInfo)
            } else {
                val index = mViewModel.mNavPathList.indexOf(topInfo)
                if (index >= 0) {
                    mViewModel.mNavPathList.subList(index + 1, mViewModel.mNavPathList.size).clear()
                } else {
                    mViewModel.mNavPathList.add(topInfo)
                }
            }
        } else {
            mViewModel.mNavPathList.clear()
        }
        mViewModel.mNavAdapter?.notifyDataSetChanged()
    }

四、权限处理与文件的操作

到此,UI的部分就大致完结了,咱们需求对数据与权限的逻辑做处理,咱们为了演示之前文章中 FilrProvider 与 DocumentsProvider 的运用,这儿用做高版别的作为展现。

首要咱们需求处理动态权限问题,分为不同的版别的权限恳求完结:

public class PermissionUtil {
    //一致处理权限
    public static boolean isStoragePermissionGranted(Activity activity) {
        Context context = activity.getApplicationContext();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            if (!Environment.isExternalStorageManager()) {
                Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
                Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
                intent.setData(uri);
                activity.startActivityForResult(intent, 1);
                return false;
            } else {
                // 有外部存储的权限
                return true;
            }
        } else {
            int readPermissionCheck = ContextCompat.checkSelfPermission(context,
                    Manifest.permission.READ_EXTERNAL_STORAGE);
            int writePermissionCheck = ContextCompat.checkSelfPermission(context,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE);
            if (readPermissionCheck == PackageManager.PERMISSION_GRANTED
                    && writePermissionCheck == PackageManager.PERMISSION_GRANTED) {
                Log.v("permission", "Permission is granted");
                return true;
            } else {
                Log.v("permission", "Permission is revoked");
                ActivityCompat.requestPermissions(activity, new String[]{
                        Manifest.permission.READ_EXTERNAL_STORAGE,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
                return false;
            }
        }
    }
}

那么在 Activity 的权限回调中咱们需求处理成功的回调:

    //动态权限授权的回调
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            obtainByPath(mViewModel.rootPath)
        }
    }
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == 1) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
                // 用户现已授权,履行需求拜访外部存储的操作
                obtainByPath(mViewModel.rootPath)
            } else {
                // 用户未授权,无法拜访外部存储
                Toast.makeText(this, "未授权,无法拜访外部存储", Toast.LENGTH_SHORT).show()
            }
        }
    }

关于文件的类型处理,咱们运用东西类封装一下,大致的逻辑是依据文件的后缀名匹配。而且对文件的 mimeType 做了匹配,大致代码如下:


    // Audio
    public static final int FILE_TYPE_MP3 = 1;
    public static final int FILE_TYPE_M4A = 2;
    public static final int FILE_TYPE_WAV = 3;
    public static final int FILE_TYPE_AMR = 4;
    public static final int FILE_TYPE_AWB = 5;
    public static final int FILE_TYPE_WMA = 6;
    public static final int FILE_TYPE_OGG = 7;
    private static final int FIRST_AUDIO_FILE_TYPE = 0;
    private static final int LAST_AUDIO_FILE_TYPE = 10;
    // MIDI
    public static final int FILE_TYPE_MID = 11;
    public static final int FILE_TYPE_SMF = 12;
    public static final int FILE_TYPE_IMY = 13;
    private static final int FIRST_MIDI_FILE_TYPE = 10;
    private static final int LAST_MIDI_FILE_TYPE = 20;
    // Video
    public static final int FILE_TYPE_MP4 = 21;
    public static final int FILE_TYPE_M4V = 22;
    public static final int FILE_TYPE_3GPP = 23;
    public static final int FILE_TYPE_3GPP2 = 24;
    public static final int FILE_TYPE_WMV = 25;
    private static final int FIRST_VIDEO_FILE_TYPE = 20;
    private static final int LAST_VIDEO_FILE_TYPE = 30;
    // Image
    public static final int FILE_TYPE_JPEG = 31;
    public static final int FILE_TYPE_GIF = 32;
    public static final int FILE_TYPE_PNG = 33;
    public static final int FILE_TYPE_BMP = 34;
    public static final int FILE_TYPE_WBMP = 35;
    private static final int FIRST_IMAGE_FILE_TYPE = 30;
    private static final int LAST_IMAGE_FILE_TYPE = 40;
    // Playlist
    public static final int FILE_TYPE_M3U = 41;
    public static final int FILE_TYPE_PLS = 42;
    public static final int FILE_TYPE_WPL = 43;
    private static final int FIRST_PLAYLIST_FILE_TYPE = 40;
    private static final int LAST_PLAYLIST_FILE_TYPE = 50;
    //TEXT
    public static final int FILE_TYPE_TXT = 51;
    public static final int FILE_TYPE_DOC = 52;
    public static final int FILE_TYPE_RTF = 53;
    public static final int FILE_TYPE_LOG = 54;
    public static final int FILE_TYPE_CONF = 55;
    public static final int FILE_TYPE_SH = 56;
    public static final int FILE_TYPE_XML = 57;
    public static final int FILE_TYPE_DOCX = 58;
    private static final int FIRST_TEXT_FILE_TYPE = 50;
    private static final int LAST_TEXT_FILE_TYPE = 60;
    //XLS
    public static final int FILE_TYPE_XLS = 61;
    public static final int FILE_TYPE_XLSX = 62;
    private static final int FIRST_XLS_FILE_TYPE = 60;
    private static final int LAST_XLS_FILE_TYPE = 70;
    //PPT
    public static final int FILE_TYPE_PPT = 71;
    public static final int FILE_TYPE_PPTX = 72;
    private static final int FIRST_PPT_FILE_TYPE = 70;
    private static final int LAST_PPT_FILE_TYPE = 80;
    //PDF
    public static final int FILE_TYPE_PDF = 81;
    private static final int FIRST_PDF_FILE_TYPE = 80;
    private static final int LAST_PDF_FILE_TYPE = 90;
    //静态内部类
    static class MediaFileType {
        int fileType;
        String mimeType;
        MediaFileType(int fileType, String mimeType) {
            this.fileType = fileType;
            this.mimeType = mimeType;
        }
    }
    private static HashMap<String, MediaFileType> sFileTypeMap
            = new HashMap<>();
    private static HashMap<String, Integer> sMimeTypeMap
            = new HashMap<>();
    static void addFileType(String extension, int fileType, String mimeType) {
        sFileTypeMap.put(extension, new MediaFileType(fileType, mimeType));
        sMimeTypeMap.put(mimeType, fileType);
    }
    static {
        //依据文件后缀名匹配
        addFileType("MP3", FILE_TYPE_MP3, "audio/mpeg");
        addFileType("M4A", FILE_TYPE_M4A, "audio/mp4");
        addFileType("WAV", FILE_TYPE_WAV, "audio/x-wav");
        addFileType("AMR", FILE_TYPE_AMR, "audio/amr");
        addFileType("AWB", FILE_TYPE_AWB, "audio/amr-wb");
        addFileType("WMA", FILE_TYPE_WMA, "audio/x-ms-wma");
        addFileType("OGG", FILE_TYPE_OGG, "application/ogg");
        addFileType("MID", FILE_TYPE_MID, "audio/midi");
        addFileType("XMF", FILE_TYPE_MID, "audio/midi");
        addFileType("RTTTL", FILE_TYPE_MID, "audio/midi");
        addFileType("SMF", FILE_TYPE_SMF, "audio/sp-midi");
        addFileType("IMY", FILE_TYPE_IMY, "audio/imelody");
        addFileType("MP4", FILE_TYPE_MP4, "video/mp4");
        addFileType("M4V", FILE_TYPE_M4V, "video/mp4");
        addFileType("3GP", FILE_TYPE_3GPP, "video/3gpp");
        addFileType("3GPP", FILE_TYPE_3GPP, "video/3gpp");
        addFileType("3G2", FILE_TYPE_3GPP2, "video/3gpp2");
        addFileType("3GPP2", FILE_TYPE_3GPP2, "video/3gpp2");
        addFileType("WMV", FILE_TYPE_WMV, "video/x-ms-wmv");
        addFileType("JPG", FILE_TYPE_JPEG, "image/jpeg");
        addFileType("JPEG", FILE_TYPE_JPEG, "image/jpeg");
        addFileType("GIF", FILE_TYPE_GIF, "image/gif");
        addFileType("PNG", FILE_TYPE_PNG, "image/png");
        addFileType("BMP", FILE_TYPE_BMP, "image/x-ms-bmp");
        addFileType("WBMP", FILE_TYPE_WBMP, "image/vnd.wap.wbmp");
        addFileType("M3U", FILE_TYPE_M3U, "audio/x-mpegurl");
        addFileType("PLS", FILE_TYPE_PLS, "audio/x-scpls");
        addFileType("WPL", FILE_TYPE_WPL, "application/vnd.ms-wpl");
        addFileType("TXT", FILE_TYPE_TXT, "text/plain");
        addFileType("RTF", FILE_TYPE_RTF, "application/rtf");
        addFileType("LOG", FILE_TYPE_LOG, "text/plain");
        addFileType("CONF", FILE_TYPE_CONF, "text/plain");
        addFileType("SH", FILE_TYPE_SH, "text/plain");
        addFileType("XML", FILE_TYPE_XML, "text/plain");
        addFileType("DOC", FILE_TYPE_DOC, "application/msword");
        addFileType("DOCX", FILE_TYPE_DOCX, "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
        addFileType("XLS", FILE_TYPE_XLS, "application/vnd.ms-excel application/x-excel");
        addFileType("XLSX", FILE_TYPE_XLSX, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        addFileType("PPT", FILE_TYPE_PPT, "application/vnd.ms-powerpoint");
        addFileType("PPTX", FILE_TYPE_PPTX, "application/vnd.openxmlformats-officedocument.presentationml.presentation");
        addFileType("PDF", FILE_TYPE_PDF, "application/pdf");
        StringBuilder builder = new StringBuilder();
        for (String s : sFileTypeMap.keySet()) {
            if (builder.length() > 0) {
                builder.append(',');
            }
            builder.append(s);
        }
        sFileExtensions = builder.toString();
    }
    public static final String UNKNOWN_STRING = "<unknown>";
    public static boolean isAudioFileType(int fileType) {
        return ((fileType >= FIRST_AUDIO_FILE_TYPE &&
                fileType <= LAST_AUDIO_FILE_TYPE) ||
                (fileType >= FIRST_MIDI_FILE_TYPE &&
                        fileType <= LAST_MIDI_FILE_TYPE));
    }
    public static boolean isVideoFileType(int fileType) {
        return (fileType >= FIRST_VIDEO_FILE_TYPE &&
                fileType <= LAST_VIDEO_FILE_TYPE);
    }
    public static boolean isImageFileType(int fileType) {
        return (fileType >= FIRST_IMAGE_FILE_TYPE &&
                fileType <= LAST_IMAGE_FILE_TYPE);
    }
    public static boolean isPlayListFileType(int fileType) {
        return (fileType >= FIRST_PLAYLIST_FILE_TYPE &&
                fileType <= LAST_PLAYLIST_FILE_TYPE);
    }
    public static boolean isTextFileType(int fileType) {
        return (fileType >= FIRST_TEXT_FILE_TYPE &&
                fileType <= LAST_TEXT_FILE_TYPE);
    }
    public static boolean isXLSFileType(int fileType) {
        return (fileType >= FIRST_XLS_FILE_TYPE &&
                fileType <= LAST_XLS_FILE_TYPE);
    }
    public static boolean isPPTFileType(int fileType) {
        return (fileType >= FIRST_PPT_FILE_TYPE &&
                fileType <= LAST_PPT_FILE_TYPE);
    }
    public static boolean isPDFFileType(int fileType) {
        return (fileType >= FIRST_PDF_FILE_TYPE &&
                fileType <= LAST_PDF_FILE_TYPE);
    }
    public static MediaFileType getFileType(String path) {
        int lastDot = path.lastIndexOf(".");
        if (lastDot < 0)
            return null;
        return sFileTypeMap.get(path.substring(lastDot + 1).toUpperCase());
    }
    //依据视频文件途径判断文件类型
    public static boolean isVideoFileType(String path) {
        MediaFileType type = getFileType(path);
        if (null != type) {
            return isVideoFileType(type.fileType);
        }
        return false;
    }
    //依据音频文件途径判断文件类型
    public static boolean isAudioFileType(String path) {
        MediaFileType type = getFileType(path);
        if (null != type) {
            return isAudioFileType(type.fileType);
        }
        return false;
    }
    //依据图片文件途径判断文件类型
    public static boolean isImageFileType(String path) {
        MediaFileType type = getFileType(path);
        if (null != type) {
            return isImageFileType(type.fileType);
        }
        return false;
    }
    //依据文本文件途径判断文件类型
    public static boolean isTextFileType(String path) {
        MediaFileType type = getFileType(path);
        if (null != type) {
            return isTextFileType(type.fileType);
        }
        return false;
    }
    public static boolean isXLSFileType(String path) {
        MediaFileType type = getFileType(path);
        if (null != type) {
            return isXLSFileType(type.fileType);
        }
        return false;
    }
    public static boolean isPPTFileType(String path) {
        MediaFileType type = getFileType(path);
        if (null != type) {
            return isPPTFileType(type.fileType);
        }
        return false;
    }
    public static boolean isPDFFileType(String path) {
        MediaFileType type = getFileType(path);
        if (null != type) {
            return isPDFFileType(type.fileType);
        }
        return false;
    }

接下来咱们就能在获取文件的时分,处理好队友的格局,赋值对应展现的Icon,就能够在数据适配器上面展现了。

五、不同版别的文件获取

其实获取到对应版别权限之后,都运用 File 就能够获取到对应版别的文件信息了,这儿便于演示,所以把 Android10 以上与 Android10 以下区别开来,高版别的运用 DocumentProvider的办法完结:

运用接口+战略的办法,咱们界说不同的完结方案:

internal interface IChooseFilePolicy {
    fun getFileList(rootPath: String, callback: (fileList: List<ChooseFileInfo>, topInfo: ChooseFileInfo?) -> Unit)
}

低版别的直接获取 FileList ,留意咱们处理文件,赋值操作等都是耗时操作,所以咱们最好是在线程池中处理,大致的逻辑如下:

internal class ChooseFileLowPolicy : IChooseFilePolicy {
    override fun getFileList(rootPath: String, callback: (fileList: List<ChooseFileInfo>, topInfo: ChooseFileInfo?) -> Unit) {
        ChooseFile.config?.mExecutor?.execute {
            val listData: ArrayList<ChooseFileInfo> = ArrayList()
            val rootFile = File(rootPath)
            var topInfo: ChooseFileInfo? = null
            val rootExternalPath = Environment.getExternalStorageDirectory().absolutePath
            if (rootExternalPath != rootPath) {
                //增加一个顶部的导航目标
                topInfo = ChooseFileInfo().apply {
                    fileName = rootFile.name
                    filePath = rootFile.absolutePath
                    isDir = true
                }
            }
            val listFiles = rootFile.listFiles()
            if (listFiles.isNullOrEmpty()) {
                //空数据回调
                callback(listData, topInfo)
                return@execute
            }
            for (file in listFiles) {
                if (file.isDirectory) {
                    //假如是文件夹
                    listData.add(
                        ChooseFileInfo().apply {
                            isDir = true
                            fileName = file.name
                            filePath = file.absolutePath
                            fileLastUpdateTime = TimeUtil.getDateInString(Date(file.lastModified()))
                            fileSize = "共" + FileUtil.getSubfolderNum(file.absolutePath) + "项"
                            fileType = ChooseFile.FILE_TYPE_FOLDER
                            fileTypeIconRes = R.drawable.file_folder
                        }
                    )
                } else {
                    //依据后缀类型封装自界说文件Bean
                    val fileInfo = ChooseFileInfo()
                    if (FileUtil.isAudioFileType(file.absolutePath)) {
                        fileInfo.fileType = ChooseFile.FILE_TYPE_AUDIO
                        fileInfo.fileTypeIconRes = R.drawable.file_audio
                    } else if (FileUtil.isImageFileType(file.absolutePath)) {
                        fileInfo.fileType = ChooseFile.FILE_TYPE_IMAGE
                        fileInfo.fileTypeIconRes = R.drawable.file_image
                    } else if (FileUtil.isVideoFileType(file.absolutePath)) {
                        fileInfo.fileType = ChooseFile.FILE_TYPE_VIDEO
                        fileInfo.fileTypeIconRes = R.drawable.file_video
                    } else if (FileUtil.isTextFileType(file.absolutePath)) {
                        fileInfo.fileType = ChooseFile.FILE_TYPE_TEXT
                        fileInfo.fileTypeIconRes = R.drawable.file_text
                    } else if (FileUtil.isXLSFileType(file.absolutePath)) {
                        fileInfo.fileType = ChooseFile.FILE_TYPE_XLS
                        fileInfo.fileTypeIconRes = R.drawable.file_excel
                    } else if (FileUtil.isPPTFileType(file.absolutePath)) {
                        fileInfo.fileType = ChooseFile.FILE_TYPE_PPT
                        fileInfo.fileTypeIconRes = R.drawable.file_ppt
                    } else if (FileUtil.isPDFFileType(file.absolutePath)) {
                        fileInfo.fileType = ChooseFile.FILE_TYPE_PDF
                        fileInfo.fileTypeIconRes = R.drawable.file_pdf
                    } else {
                        fileInfo.fileType = ChooseFile.FILE_TYPE_Unknown
                        fileInfo.fileTypeIconRes = R.drawable.file_unknown
                    }
                    fileInfo.apply {
                        isDir = false
                        fileName = file.name
                        filePath = file.absolutePath
                        filePathUri = getFileUri(ChooseFile.activityRef?.get(), file).toString()
                        fileLastUpdateTime = TimeUtil.getDateInString(Date(file.lastModified()))
                        fileSize = FileUtil.getFileSize(file.length())
                    }
                    listData.add(fileInfo)
                }
            }
            //满数据回调
            callback(filterData, topInfo)
        }
    }
}

Android 10以上的高版别咱们发动 DocumentProvider 的查询办法:

internal class ChooseFileHighPolicy : IChooseFilePolicy {
    @SuppressLint("Range")
    override fun getFileList(rootPath: String, callback: (fileList: List<ChooseFileInfo>, topInfo: ChooseFileInfo?) -> Unit) {
        val uri = DocumentsContract.buildChildDocumentsUri(
            "com.newki.choosefile.authorities",
            rootPath
        )
        ChooseFile.config?.mExecutor?.execute {
            val cursor = ChooseFile.activityRef?.get()?.contentResolver?.query(uri, null, null, null, null)
            val listData: ArrayList<ChooseFileInfo> = ArrayList()
            var topInfo: ChooseFileInfo? = null
            if (cursor != null) {
                while (cursor.moveToNext()) {
                    val isTop = cursor.getInt(cursor.getColumnIndex("isTop"))
                    val isRoot = cursor.getInt(cursor.getColumnIndex("isRoot"))
                    val fileName = cursor.getString(cursor.getColumnIndex("fileName"))
                    val isDir = cursor.getInt(cursor.getColumnIndex("isDir"))
                    val fileSize = cursor.getString(cursor.getColumnIndex("fileSize"))
                    val fileLastUpdateTime = cursor.getString(cursor.getColumnIndex("fileLastUpdateTime"))
                    val filePath = cursor.getString(cursor.getColumnIndex("filePath"))
                    val filePathUri = cursor.getString(cursor.getColumnIndex("filePathUri"))
                    val fileTypeIconRes = cursor.getInt(cursor.getColumnIndex("fileTypeIconRes"))
                    if (isTop == 1) {
                        if (isRoot == 0) {
                            topInfo = ChooseFileInfo().apply {
                                this.fileName = fileName
                                this.isDir = isDir != 0
                                this.fileSize = fileSize
                                this.fileLastUpdateTime = fileLastUpdateTime
                                this.filePath = filePath
                                this.filePathUri = filePathUri
                                this.fileTypeIconRes = fileTypeIconRes
                            }
                        }
                    } else {
                        listData.add(ChooseFileInfo().apply {
                            this.fileName = fileName
                            this.isDir = isDir != 0
                            this.fileSize = fileSize
                            this.fileLastUpdateTime = fileLastUpdateTime
                            this.filePath = filePath
                            this.filePathUri = filePathUri
                            this.fileTypeIconRes = fileTypeIconRes
                        })
                    }
                }
                cursor.close()
                //满数据回调
                callback(filterData, topInfo)
            } else {
                callback(emptyList(), null)
            }
        }
    }
}

而 DocumentProvider 的详细完结如下,咱们只需求要点关注 queryChildDocuments 办法的完结即可:

public class ChooseFileDocumentProvider extends DocumentsProvider {
    private final static String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{"isTop", "isRoot", "fileName", "isDir", "fileSize", "fileLastUpdateTime",
            "filePath", "filePathUri", "fileType", "fileTypeIconRes"};
    @Override
    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
        return null;
    }
    @Override
    public boolean isChildDocument(String parentDocumentId, String documentId) {
        return documentId.startsWith(parentDocumentId);
    }
    @Override
    public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
        // 创立一个查询cursor, 来设置需求查询的项, 假如"projection"为空, 那么运用默许项
        final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
        includeFile(result, new File(documentId), false, false);
        return result;
    }
    @Override
    public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
        // 创立一个查询cursor, 来设置需求查询的项, 假如"projection"为空, 那么运用默许项。
        final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
        final File parent = new File(parentDocumentId);
        boolean isDirectory = parent.isDirectory();
        boolean canRead = parent.canRead();
        File[] files = parent.listFiles();
        boolean isRoot = parent.getAbsolutePath().equals(Environment.getExternalStorageDirectory().getAbsolutePath());
        includeFile(result, parent, isRoot, true);
        //遍历增加处理文件列表
        if (isDirectory && canRead && files != null && files.length > 0) {
            for (File file : files) {
                // 增加文件的名字, 类型, 巨细等特点
                includeFile(result, file, isRoot, false);
            }
        }
        return result;
    }
    private void includeFile(final MatrixCursor result, final File file, boolean isRoot, boolean isTop) {
        final MatrixCursor.RowBuilder row = result.newRow();
        row.add("isTop", isTop ? "1" : "0");
        row.add("isRoot", isRoot ? "1" : "0");
        if (file.isDirectory()) {
            row.add("fileName", file.getName());
            row.add("isDir", 1);
            row.add("fileSize", "共" + FileUtil.getSubfolderNum(file.getAbsolutePath()) + "项");
            row.add("fileLastUpdateTime", TimeUtil.getDateInString(new Date(file.lastModified())));
            row.add("filePath", file.getAbsolutePath());
            row.add("filePathUri", file.getAbsolutePath());
            row.add("fileType", ChooseFile.FILE_TYPE_FOLDER);
            row.add("fileTypeIconRes", R.drawable.file_folder);
        } else {
            row.add("fileName", file.getName());
            row.add("isDir", 0);
            row.add("fileSize", FileUtil.getFileSize(file.length()));
            row.add("fileLastUpdateTime", TimeUtil.getDateInString(new Date(file.lastModified())));
            row.add("filePath", file.getAbsolutePath());
            row.add("filePathUri", getFileUri(ChooseFile.activityRef.get(), file).toString());
            setFileType(row, file.getAbsolutePath());
        }
    }
    private void setFileType(MatrixCursor.RowBuilder row, String absolutePath) {
        if (FileUtil.isAudioFileType(absolutePath)) {
            row.add("fileType", ChooseFile.FILE_TYPE_AUDIO);
            row.add("fileTypeIconRes", R.drawable.file_audio);
        } else if (FileUtil.isImageFileType(absolutePath)) {
            row.add("fileType", ChooseFile.FILE_TYPE_IMAGE);
            row.add("fileTypeIconRes", R.drawable.file_image);
        } else if (FileUtil.isVideoFileType(absolutePath)) {
            row.add("fileType", ChooseFile.FILE_TYPE_VIDEO);
            row.add("fileTypeIconRes", R.drawable.file_video);
        } else if (FileUtil.isTextFileType(absolutePath)) {
            row.add("fileType", ChooseFile.FILE_TYPE_TEXT);
            row.add("fileTypeIconRes", R.drawable.file_text);
        } else if (FileUtil.isXLSFileType(absolutePath)) {
            row.add("fileType", ChooseFile.FILE_TYPE_XLS);
            row.add("fileTypeIconRes", R.drawable.file_excel);
        } else if (FileUtil.isPPTFileType(absolutePath)) {
            row.add("fileType", ChooseFile.FILE_TYPE_PPT);
            row.add("fileTypeIconRes", R.drawable.file_ppt);
        } else if (FileUtil.isPDFFileType(absolutePath)) {
            row.add("fileType", ChooseFile.FILE_TYPE_PDF);
            row.add("fileTypeIconRes", R.drawable.file_pdf);
        } else {
            row.add("fileType", ChooseFile.FILE_TYPE_Unknown);
            row.add("fileTypeIconRes", R.drawable.file_unknown);
        }
    }
    @Override
    public String getDocumentType(String documentId) throws FileNotFoundException {
        return null;
    }
    @Override
    public ParcelFileDescriptor openDocument(String documentId, String mode, @Nullable CancellationSignal signal) throws FileNotFoundException {
        return null;
    }
    @Override
    public boolean onCreate() {
        return true;
    }
}

记得要在清单文件中注册哦:

        <provider
            android:name=".provider.ChooseFileDocumentProvider"
            android:authorities="com.newki.choosefile.authorities"
            android:exported="true"
            android:grantUriPermissions="true"
            android:permission="android.permission.MANAGE_DOCUMENTS">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>

为了地址的可达性,对应 7.0以上的版别咱们最好是供给到 Uri 的资源,所以咱们界说到自己的 FileProvider ,而咱们只用到了外置 SD 卡的资源,所以咱们直接这么装备即可:

        <provider
            android:name=".provider.ChooseFileProvider"
            android:authorities="com.newki.choosefile.file.path.share"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/choose_file_paths" />
        </provider>

关于 FileProvider 的细节运用能够看我的这一篇文章【别滥用FileProvider了,Android中FileProvider的各种场景运用】

运用起来的话,就都是这么固定的写法:

    private Uri getFileUri(Context context, File file) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return ChooseFileProvider.getUriForFile(context, "com.newki.choosefile.file.path.share", file);
        } else {
            return Uri.fromFile(file);
        }
    }

六、过滤的操作

关于咱们的运用来说,咱们只需求选中SD卡中的文档文件,Txt,word,pdf等文件,那么咱们就一定是需求过滤的操作的。

因为在上文咱们获取File,封装自界说的 Bean 目标 ChooseFileInfo 中咱们现已把文件的自界说格局界说好了,所以咱们再回调之前先进行过滤操作,然后在再排序之后回来最终的数据源即可。

而为了对过滤的信息进行更灵活的过滤,咱们能够直接露出 ChooseFileInfo 目标,这样咱们甚至能依据文件类型,文件称号,文件最终操作时刻等等的办法进行过滤了。

先界说一个过滤的笼统接口如下:

public interface IFileTypeFilter {
    List<ChooseFileInfo> doFilter(List<ChooseFileInfo> list);
}

在 FileConfig 的装备中,咱们能够加上过滤的接口处理逻辑


class ChooseFileConfig(private val chooseFile: ChooseFile) {
    internal var mIFileTypeFilter: IFileTypeFilter? = null
        fun setTypeFilter(filter: IFileTypeFilter): ChooseFileConfig {
        mIFileTypeFilter = filter
        return this
    }
    ...
}

咱们在最终回来的时分就能够这样:


    override fun getFileList(rootPath: String, callback: (fileList: List<ChooseFileInfo>, topInfo: ChooseFileInfo?) -> Unit) {
        ChooseFile.config?.mExecutor?.execute {
            // ... 获取文件操作
            //依据Filter过滤数据并排序
            val filterData = ChooseFile.config?.mIFileTypeFilter?.doFilter(listData) ?: listData
            FileUtil.SortFilesByInfo(filterData)
            //满数据回调
            callback(filterData, topInfo)
        }
    }

而排序的逻辑就是先展现文件夹,然后依据文件名排序:


    public static void SortFilesByInfo(List<ChooseFileInfo> fileList) {
        Collections.sort(fileList, (o1, o2) -> {
            if (o1.isDir && (!o2.isDir))
                return -1;
            if ((!o1.isDir) && o2.isDir)
                return 1;
            return Collator.getInstance(java.util.Locale.CHINA).compare(o1.fileName, o2.fileName);
        });
    }

到此咱们的全体的根本结构就完结了。

七、运用与上传

先看看详细的运用办法:


        findViewById<Button>(R.id.btn_get_file).setOnClickListener {
            ChooseFile.create(this)
                .setUIConfig(ChooseFileUIConfig.Builder().build())
                .setTypeFilter { listData ->
                    return@setTypeFilter ArrayList(listData.filter { item ->
                        //只需文件夹
//                          item.isDir
                        //只需文档文件
                        item.fileType == ChooseFile.FILE_TYPE_FOLDER ||
                                item.fileType == ChooseFile.FILE_TYPE_TEXT ||
                                item.fileType == ChooseFile.FILE_TYPE_PDF
                    })
                }
                .forResult {
                    Toast.makeText(this, "选中的文件:" + it?.fileName, Toast.LENGTH_SHORT).show()
                    val uri = Uri.parse(it?.filePathUri)
                    val fis = contentResolver.openInputStream(uri)
                    Log.w("TAG", "文件的Uri:" + it?.filePathUri + " uri:" + uri + " fis:" + fis)
                    fis?.close()
                }
        }

这儿咱们能够拿到path或uri,拿到 uri 之后咱们能够直接获取输入流,上传到后端服务器,例如:

public interface ApiService {
    @Multipart
    @POST("upload")
    Call<ResponseBody> upload(@Part("text") String text, @Part("file") RequestBody requestBody);
}
// 创立OkHttpClient实例
OkHttpClient client = new OkHttpClient();
// 构建恳求体
RequestBody fileRequestBody = new RequestBody() {
    @Override
    public MediaType contentType() {
        return MediaType.parse("application/octet-stream");
    }
    @Override
    public long contentLength() {
        try {
            // 回来输入流的长度,假如无法确定长度,回来-1
            return inputStream.available();
        } catch (IOException e) {
            return -1;
        }
    }
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        // 将输入流中的数据写入到恳求体中
        Source source = Okio.source(inputStream);
        sink.writeAll(source);
    }
};
// 创立Retrofit实例
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://www.example.com/")
        .client(client)
        .build();
// 创立ApiService实例
ApiService apiService = retrofit.create(ApiService.class);
// 构造恳求参数
String text = "Hello World!";
// 发送恳求并获取响应
Call<ResponseBody> call = apiService.upload(text, fileRequestBody);
Response<ResponseBody> response = call.execute();

下面看看视频的演示:

从零开始实现一个兼容版本的Android文件选择器

后记

咱们界说的文件挑选结构只是一个简略的轻量级结构,甚至都没有加入多选文件的操作,创立文件、修正文件的操作等。为什么?只因咱们没这方面的需求罢了。

多选文件的操作只需求修正一些UI和一些选中的逻辑罢了并不杂乱,创立修正文件则需求 DocumentFile 配合 SAF 的操作才干兼容高版别,略微杂乱一些,之前的文章也讲过,假如咱们有爱好也能够自行完结。究竟我也只需求一个文件挑选的功用罢了,不想过渡封装。

相似的结构,咱们除了做一些文件挑选的功用,相似的图片挑选也能够采用相似的结构完结,只是获取图片的办法不同罢了。

经过本文咱们能够了解File的运用,权限的恳求,FileProvider的运用,以及要点的DocumentProvider,咱们重写并完整的了解了怎么的运用。

接下来放出源码供咱们参考与指正,【传送门】。

本文发布之时也现已传到 MavenCentral 了,如有要求能够直接依靠,地址如下:

implementation “com.gitee.newki123456:android_choose_file:1.0.0”

内部的依靠库版别并不高,appcompat:1.2.0 ,recyclerview:1.1.0 ,swiperefreshlayout:1.1.0 。最低支撑到 4.4 版别,默许 target 为 31 。aar总巨细为 100K 。假如有需求能够长途依靠去运用,假如有自界说化的需求,也能够自行拉代码修正。

惯例,我如有讲解不到位或错漏的当地,期望同学们能够指出。假如有更好的办法或其他办法,或许你有遇到的坑也都能够在评论区交流一下,咱们互相学习进步。

为了开源项目把公司项目进度都落下了,接下来要赶赶进度了…

假如感觉本文对你有一点点的帮助,还望你能点赞支撑一下,你的支撑是我最大的动力。

Ok,这一期就此完结。

从零开始实现一个兼容版本的Android文件选择器

本文正在参加「金石计划」