# ChooseFile
**Repository Path**: semdy/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**: 0
- **Forks**: 14
- **Created**: 2024-06-24
- **Last Updated**: 2024-06-24
## 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 效果:

Android 9.0 效果:

Android 12 效果:

项目实现基于 target31,不配置兼容模式 requestLegacyExternalStorage ,可用于 4.4 版本以上Android系统,可保持UI的一致性。
说了这么多还是赶紧开始吧!

### 一、文件选择的页面的配置
我们使用我们自定义的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