# ChooseFile **Repository Path**: newki123456/ChooseFile ## Basic Information - **Project Name**: ChooseFile - **Description**: Android选择文件 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 36 - **Forks**: 14 - **Created**: 2023-04-04 - **Last Updated**: 2025-04-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Android文件选择器的实现 ## 前言 之前发布的项目都是不基于 Activity 实现的,只是一些功能性的小功能,而类似文件选择,图片选择,除了功能的实现还需要处理 UI 相关的配置。 在前面的[【如何操作文件的选择】](https://juejin.cn/post/7211422882253537339) 一文中我就想把逻辑做一下封装,做成开箱即用的文件选择器,本来这功能是项目中自用的,UI 等都是自有的,如果要做开源出去那必要要大改,设置可配置选项。 那么如何自定义一个文件下载器呢? 1. 我们需要配置 Activity 基本的 Theme,动画,状态栏,导航栏等处理。 2. 我们需要配置展示的文本大小,返回图标,列表与导航栏的文本大小等等。 3. 然后我们对XML的布局并构建导航列表与文件列表的数据适配器等。 4. 然后我们就可以处理权限以及对文件的操作了。 5. 可以使用策略模式不同的版本不同的实现方式。 6. 过滤操作是比不可少的,我们获取文件之后使用过滤操作展示我们想要的文件。 差不多到处就能完成一个基本的操作文件选择框架了,具体实现的效果如下: Android 7.0 效果: ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/150f70466a6d4f97b7bf056717f484b0~tplv-k3u1fbpfcp-watermark.image?) Android 9.0 效果: ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aae254d8b4ef49a2ac69c2119fe6c4b8~tplv-k3u1fbpfcp-watermark.image?) Android 12 效果: ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c9ea301d1fb347a5a15f7344ae5fadc8~tplv-k3u1fbpfcp-watermark.image?) 项目实现基于 target31,不配置兼容模式 requestLegacyExternalStorage ,可用于 4.4 版本以上Android系统,可保持UI的一致性。 说了这么多还是赶紧开始吧! ![300.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e526cc29ee84b22911915126f8561bf~tplv-k3u1fbpfcp-watermark.image?) ### 一、文件选择的页面的配置 我们使用我们自定义的theme与动画即可。由于我们要自己实现可控的标题栏,所以我们的样式不需要toolbar: ```xml ``` 为了可配置的状态栏与导航栏,这里我用到之前的项目中的 StatusBarHost 框架,具体的实现与细节可以查看之前的文章,[【传送门】](https://juejin.cn/post/7138312796474703909)。 那么我们创建选择文件的Activity大致如下: ```kotlin 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保存一些页面状态: ```kotlin class ChooseFileViewModel : ViewModel() { val mNavPathList = arrayListOf() var mNavAdapter: FileNavAdapter? = null val mFileList = arrayListOf() var mFileListAdapter: FileListAdapter? = null //根目录 val rootPath = Environment.getExternalStorageDirectory().absolutePath var rootChoosePos = 0 //根目录文档选中的索引 //当前选择的路径 var mCurPath = Environment.getExternalStorageDirectory().absolutePath } ``` 这里已经用到了一些UI的配置选项,我们赶紧接下来往下走。 ### 二、页面的UI配置与其他配置 一般我们都会根据不同的UI效果,设置不同的文本颜色和背景,所以我们需要把页面上的文本与背景和图标等选项抽取出来,配置成可选的属性: ```java 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() { } ... } ``` 然后我们使用构建者模式创建可选的配置,如果不选择那么就可以使用默认的配置,就特别适合此场景: ```java 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的配置完成之后,我们还需要对一些常规的配置做一些可选操作,例如线程池的自定义,过滤文件的选择等等。 ```java 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 { 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 则是我们的单例使用入口,内部实现如下: ```kotlin object ChooseFile { @JvmField internal var activityRef: WeakReference? = 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 } } ``` 到处我们就可以正常的使用框架了: ```kotlin findViewById