-
使用最新版的 filepicker 库,必须使用最新版的xwidget库。都是 5.8.1 以上确保。
-
用了很多,都没有丝滑的选择效果的开源库,滑动选择丝滑。闲来没事,随手写一个吧。
-
图库选择,文件选择
-
仿华为相册滑动选择手势逻辑,
-
微信选择库样式风格
-
支持多选,限制数量,不限制数量。
-
选择条件可限定:图片 / 视频各自限量(如图片 9 张、视频 1 个),可以要求必须选够, 也能让图片和视频只能选其中一类(互斥)。见「按类型限量」一节。
-
使用场景,多用于 视频剪辑选择库,相册管理选择库,朋友圈选图或者视频。
-
或者:点击下载 app-debug.apk
- 下面的这个三方库,本maven仓库中的aar都过滤掉了。建议自己从下面引用,防止库冲突。
- **filepicker
** - **xwidget
** - **BRV
** - **BasePopup
** - androidx.media3 这里建议使用1.11.0
稳定版 - glide
dependencies {
// 基础组件库 (必须)
implementation("com.gitee.pichs:filepicker:7.5.0")
// 基础组件库 (必须)
implementation("com.gitee.pichs:xwidget:5.8.1")
// glide 图片加载 (必须)
implementation("com.github.bumptech.glide:glide:4.16.0")
//基础库(必须)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.material)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.annotation)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
// brv (必须)
implementation("com.github.liangjingkanji:BRV:1.6.1")
// 弹窗 (必须)
implementation("io.github.razerdp:BasePopup:3.2.1")
// 视频播放库 (必须)采用exoplayer
implementation("androidx.media3:media3-exoplayer:1.7.1")
implementation("androidx.media3:media3-ui:1.7.1")
}
使用libs.version.toml用户引入方式
dependencies {
api(libs.filepicker)
api(libs.xwidget)
api(libs.glide)
api(libs.brv)
api(libs.basepopup)
api(libs.media3.exoplayer)
api(libs.media3.ui)
}
# libs.version.toml中写法
[versions]
xwidget="5.8.1"
filepicker="7.5.0"
brv="1.6.1"
basepopup="3.2.1"
glide="4.16.0"
activityKtx="1.9.0"
fragmentKtx="1.6.2"
recyclerview="1.4.0"
kotlinxCoroutinesAndroid="1.7.3"
kotlinxCoroutinesCore="1.7.3"
media3Exoplayer="1.7.1"
[libraries]
androidx-media3-exoplayer={module="androidx.media3:media3-exoplayer",version.ref="media3Exoplayer"}
androidx-media3-ui={module="androidx.media3:media3-ui",version.ref="media3Exoplayer"}
androidx-activity-ktx={group="androidx.activity",name="activity-ktx",version.ref="activityKtx"}
androidx-fragment-ktx={group="androidx.fragment",name="fragment-ktx",version.ref="fragmentKtx"}
androidx-recyclerview={group="androidx.recyclerview",name="recyclerview",version.ref="recyclerview"}
kotlinx-coroutines-android={group="org.jetbrains.kotlinx",name="kotlinx-coroutines-android",version.ref="kotlinxCoroutinesAndroid"}
kotlinx-coroutines-core={group="org.jetbrains.kotlinx",name="kotlinx-coroutines-core",version.ref="kotlinxCoroutinesCore"}
xwidget={group="com.gitee.pichs",name="xwidget",version.ref="xwidget"}
filepicker={group="com.gitee.pichs",name="filepicker",version.ref="filepicker"}
basepopup={group="io.github.razerdp",name="BasePopup",version.ref="basepopup"}
brv={group="com.github.liangjingkanji",name="BRV",version.ref="brv"}
glide={group="com.github.bumptech.glide",name="glide",version.ref="glide"}
FilePicker 是一个支持图片、视频、文件选择的轻量级文件选择器,支持自定义配置、选中列表管理、回调返回选中数据。
-
支持 Activity 和 Fragment 调用
-
支持多选、最大数量控制、文件大小限制
-
支持返回原图选项
-
支持 UI 文案与界面定制
-
支持滑动选择
-
支持单选立即返回
-
支持预选列表
FilePicker.with(this) // this: FragmentActivity
.setSelectType(FilePickerSelectType.ofImage()) // 设置选择类型
.setMaxSelectNumber(9) // 设置最大选择数量
.setSlideChooseEnable(true) // 开启滑动选择
.setOnSelectCallback(object : OnSelectCallback {
override fun onSelectedCallback(isUseOriginal: Boolean, list: MutableList<MediaEntity>) {
// 选择完成回调
println("是否使用原图: $isUseOriginal")
list.forEach {
println("文件路径: ${it.path}")
}
}
override fun onCancel() {
// 取消选择
}
})
.setUiConfig(
FilePickerUIConfig(
isHideSelectTab = false,
allAlbumName = "全部",
confirmBtnText = "发送",
isShowOriginal = false,
isPreviewPageIndexMode = true,
isShowSelectedListDeleteIcon = true,
folderNickNameMap = hashMapOf(
"DCIM" to "相册"
)
)
).
.start()FilePicker.with(this) // this: Fragment
.setSelectType(FilePickerSelectType.ofImage())
.setMaxSelectNumber(5)
.setMaxFileSize(50 * 1024 * 1024) // 50MB
.setMinFileSize(1 * 1024) // 1KB
.setOnSelectCallback { isUseOriginal, list ->
// 选择完成回调
}
.setUiConfig(
FilePickerUIConfig(
isHideSelectTab = true,
allAlbumName = "全部",
confirmBtnText = "下一步",
isShowOriginal = false,
isPreviewPageIndexMode = true,
isShowSelectedListDeleteIcon = true,
folderNickNameMap = hashMapOf(
"DICM" to "相机",
"Download" to "下载"
)
)
).
.start()FilePicker.with(this)
.setSelectType(FilePickerSelectType.ofAudio()) // 文件类型:ofAudio/ofDocument/ofPdf/ofApk/ofZipAll...
.setMaxSelectNumber(1)
.setSingleClickEnable(true) // 单击直接返回,文件选择场景常用
.setOnSelectCallback { isUseOriginal, list ->
// 返回 MediaEntity(含 path/name/mimeType/size)
}
.start()任意后缀组合,不需要为每种后缀单独加类型:
FilePicker.with(this)
.setSelectType(FilePickerSelectType.ofExtensions("xz", "tar", "bak"))
.start()有专属类型入口的后缀(图片、音频、文档、核心压缩包等)优先用
ofXxx(),
两者在 UI 模式与预览上的差异见下方 类型入口怎么选。
详见下方 自定义后缀过滤。
在网格第一格放一个"相机"格子,点击调起系统相机拍照或录像,拍完自动插到列表第一个。
FilePicker.with(this)
.setSelectType(FilePickerSelectType.ofImage()) // 或 ofVideo()
.setShowCameraEntry(true)
.start()默认 false:不开启时这条链路完全不参与 —— 不占列表格子、不注册 viewType、不请求权限,
对宿主等同于不存在。
生效条件:setShowCameraEntry(true),且当前类型下相机能产出点东西(图片或视频)。
GIF / 音频 / 文档不显示;有 tab 的类型里入口跟着当前 tab 走。
格子的画法按"能产出几种"决定:
| 当前生效类型 | 格子 |
|---|---|
只有图片(ofImage(),或 ofAll() 的「图片」页) |
整格 = 拍照 |
只有视频(ofVideo(),或 ofAll() 的「视频」页) |
整格 = 录像 |
图片 + 视频(ofAll() / ofAllWithGif() 的「全部」页) |
从中间劈开:左半边录像、右半边拍照 |
| 只有 GIF(「GIF」页) | 不显示入口 |
行为说明:
| 项 | 说明 |
|---|---|
| 位置 | 网格左上角第一格,随列表滚动,尺寸与相邻格子一致 |
| 点击 | 调起系统相机(拍照 ACTION_IMAGE_CAPTURE / 录像 ACTION_VIDEO_CAPTURE) |
| 拍完 | 自动插到列表第一个位置,并默认勾选上("这张就是要用的") |
| 已到上限 | 仍然插入,但不勾选(不会超过 maxSelectNumber),并 toast 提示 |
| 单选立即返回 | maxSelectNumber == 1 且开了 setSingleClickEnable(true) 时,拍完直接回调返回,\ |
拍到的这张会**替换**掉已有选择(宿主通过 `setSelectedList` 预置的那张也一样) |
| 大小超限 | 产物不符合 minFileSize / maxFileSize 时不插入、不勾选,toast 提示(文案见
cameraEntrySizeLimitToastContent) |
| 产物落地 | 写进系统相册(DCIM/Camera),和系统相机拍的照片放在一起 |
| 取消拍摄 | 会清理掉本次产生的空记录/空文件,不会在相册里留下打不开的空条目 |
| 图标与文案 | 图标默认用库内资源,可换 cameraEntryIconResId(拍照)/ cameraEntryVideoIconResId(录像);
文字标签用 cameraEntryPhotoText / cameraEntryVideoText |
权限与 manifest:
- 库不声明也不占用 CAMERA 权限 —— 拍摄由相机 App 自己完成,调用方不需要该权限。
但如果宿主 App 自己声明了android.permission.CAMERA却还没授权,ACTION_IMAGE_CAPTURE/
ACTION_VIDEO_CAPTURE会直接抛SecurityException(系统规定)。这种情况下库会先申请权限,
授权后自动继续拍摄。真机实测原话(华为 BRE-AL80 / Android 12,宿主声明了但未授权):
Permission Denial: starting Intent { act=android.media.action.VIDEO_CAPTURE ... } with revoked permission android.permission.CAMERA - 库不往宿主 manifest 里加任何东西(没有
<provider>、没有<queries>)。 - 产物写入系统相册:Android 10+ 不需要任何存储权限;Android 7.0–9 需要
WRITE_EXTERNAL_STORAGE
(与本库查询相册本身的前提一致)。
为什么不用
resolveActivity()判断有没有相机 App(官方文档推荐这么写):
宿主 targetSdk ≥ 30 时,没声明<queries>的应用会被包可见性过滤,resolveActivity()/queryIntentActivities()对这几个相机 action 一律返回 null / 0,但startActivity依然能成功。 同一台华为设备上实测:四个相机 action 的resolveActivity全是null,而实际能起com.huawei.camera/com.android.camera.VideoCamera。用resolveActivity() != null当守卫 会让相机入口永远点不动。本库因此改成startActivity包 try/catch。
setMaxSelectNumber 只能管一个总数。7.2.0 起新增按类型分别限量:图片、视频各自设上限,
可以要求"必须选够",也可以要求"图片和视频只能选其中一类"。
FilePicker.with(this)
.setSelectType(FilePickerSelectType.ofAll())
.setSelectRules(
SelectRule.ofImage(0, 9), // 图片最多 9 张(min = 0 表示不强制)
SelectRule.ofVideo(0, 1), // 视频最多 1 个
)
.start()SelectRule.ofImage(min, max) / ofVideo(min, max) / ofGif(min, max):
max:该类型的硬上限,到顶后勾不上,提示"图片最多选择2个";min > 0:必须选。提交时数量必须落在min..max内;不满足时确定按钮不高亮, 点击提示"图片必须选3个",且不会返回;- 没配规则的类型不受限制。
| 需求 | 写法 |
|---|---|
| 图片 ≤ 9、视频 ≤ 1,可混选 | setSelectRules(SelectRule.ofImage(0, 9), SelectRule.ofVideo(0, 1)) |
| 图片必须正好 9 张 | setSelectRules(SelectRule.ofImage(9, 9)) |
| 视频必须 1 个、图片最多 9 张 | setSelectRules(SelectRule.ofVideo(1, 1), SelectRule.ofImage(0, 9)) |
| 图片和视频只能选其中一类 | 上面的规则 + .setExclusive(true) |
| 合计最多 5 个(不分类) | .setMaxSelectNumber(5) |
| GIF 不占图片额度 | .setGifCountsAsImage(false) |
互斥(setExclusive(true))的锁定规则:
- 只有一类配了
min > 0→ 直接锁定那一类,另一类点了会被拦下(等于"只能选图片 / 只能选视频"); - 两类都配了
min > 0→ 先点哪类就锁定哪类,另一类的"必须选"本次作废; - 已选类型全部取消后自动解锁,可以改选另一类。
启用条件(重要):setExclusive(...) 和 setGifCountsAsImage(...) 只有在配置了
setSelectRules(...) 之后才生效。一个规则都不配时,它们会被忽略(打 warn 日志),
数量限制仍由 setMaxSelectNumber(n) 决定 —— 这样"只开互斥、没配规则"不会把你设的最大数量弄丢。
和 setMaxSelectNumber 的关系:用了规则,setMaxSelectNumber 就不生效了。
数量限制始终只有一个来源:
- 不用规则 →
setMaxSelectNumber(n)是唯一口径(老行为); - 用了规则 → 合计上限由各类型
max自动叠加得出(互斥时取单类型最大的max), 调用方不需要再写第二个数字,也就不存在"合计上限和规则冲突"这种配置。
所以规则模式下没有任何"谁优先"的抉择 —— 从设计上就没有第二个数字。
三个提示文案都可以在 FilePickerUIConfig 里改:selectTypeMaxToastContent("图片最多选择2个")、
selectExclusiveConflictToastContent("当前只能选择视频")、selectRuleNotSatisfiedToastContent
("图片必须选3个")。
预选列表(
setSelectedList)超出规则时会按规则截断并打 warn 日志,不会静默吞掉。
| 方法名 | 描述 |
|---|---|
with(activity) |
使用 FragmentActivity 初始化 |
with(fragment) |
使用 Fragment 初始化 |
convertToPathList(list) |
将 MediaEntity 列表转换为路径列表 |
convertToEntityList(list) |
将路径列表转换为 MediaEntity 列表 |
7.0.0 起类型入口方法统一收敛到
FilePickerSelectType(语义更贴合)。
FilePicker.ofImage()等老方法已标注@Deprecated,仍可正常使用(内部转发),
IDE 会提示迁移,建议尽快替换为FilePickerSelectType.ofXxx()。完整对照见
已废弃 API。
以下方法自 7.0.0 起标注 @Deprecated(源码级兼容,调用不报错、行为不变,内部直接转发到新入口)。
新代码请一律使用右列写法:
| 废弃(不要再用) | 最新写法 |
|---|---|
FilePicker.ofAll() |
FilePickerSelectType.ofAll() |
FilePicker.ofAllWithGif() |
FilePickerSelectType.ofAllWithGif() |
FilePicker.ofImage() |
FilePickerSelectType.ofImage() |
FilePicker.ofVideo() |
FilePickerSelectType.ofVideo() |
FilePicker.ofGif() |
FilePickerSelectType.ofGif() |
FilePicker.ofAudio() |
FilePickerSelectType.ofAudio() |
FilePicker.ofDocument() |
FilePickerSelectType.ofDocument() |
FilePicker.ofPdf() |
FilePickerSelectType.ofPdf() |
FilePicker.ofDoc() |
FilePickerSelectType.ofDoc() |
FilePicker.ofPpt() |
FilePickerSelectType.ofPpt() |
FilePicker.ofExcel() |
FilePickerSelectType.ofExcel() |
FilePicker.ofTxt() |
FilePickerSelectType.ofTxt() |
FilePicker.ofApk() |
FilePickerSelectType.ofApk() |
FilePicker.ofZipAll() |
FilePickerSelectType.ofZipAll() |
FilePicker.ofZip() |
FilePickerSelectType.ofZip() |
FilePicker.ofRar() |
FilePickerSelectType.ofRar() |
FilePicker.of7Z() |
FilePickerSelectType.of7Z() |
6.x 已删除的
ofTar()/ofGz()/ofBz2()/ofIso()/ofBr()/ofLz4()/ofZstd()/ofXz()
不在废弃之列(已移除),替代写法见下方 7.0.0 旧 API 迁移对照表。
| 方法名 | 描述 |
|---|---|
ofAll() |
选择图片和视频 |
ofAllWithGif() |
选择图片、视频和GIF |
ofImage() |
只选择图片 |
ofVideo() |
只选择视频 |
ofGif() |
只选择GIF |
ofAudio() |
只选择音频 |
ofDocument() |
选择所有文档类型 |
ofPdf() |
只选择PDF |
ofDoc() |
只选择DOC和DOCX |
ofPpt() |
只选择PPT和PPTX |
ofExcel() |
只选择XLS和XLSX |
ofTxt() |
只选择TXT |
ofMarkdown() |
只选择 Markdown(.md / .markdown) |
ofXmind() |
只选择 XMind 思维导图(.xmind) |
ofFreemind() |
只选择 FreeMind 思维导图(.mm) |
ofApk() |
只选择APK |
ofZipAll() |
选择所有压缩包类型 |
ofZip() |
只选择ZIP |
ofRar() |
只选择RAR |
of7Z() |
只选择7Z |
ofExtensions(...) |
按任意后缀组合自定义过滤(见下节) |
有专属类型入口的后缀,优先用类型入口;没有的才用 ofExtensions()。
两者能力上都能查到文件,但语义与体验有差别:
| 场景 | 推荐入口 | 原因 |
|---|---|---|
| 选图片/视频 | ofImage() / ofVideo() / ofAll() |
相册网格模式、大图预览、原图选项齐全 |
| 选音频/文档/APK | ofAudio() / ofDocument() / ofApk() |
专属查询条件与图标 |
| 选 Markdown/思维导图 | ofMarkdown() / ofXmind() / ofFreemind() |
按后缀查询,专属图标(见下) |
| 选压缩包 | ofZipAll() / ofZip() / ofRar() / of7Z() |
mime + 后缀双轨查询,图标专属 |
| 选其他任意后缀 | ofExtensions("xz", "bak", ...) |
无对应类型时的通用方案 |
ofExtensions() 与类型入口的差异(按后缀捞文件的语义带来的):
-
UI 呈现为文件列表模式(按时间排序 + 文件夹分组),不是相册网格模式
-
无大图预览手势(
isCanPreview()不放行ext:类型) -
查询按文件名后缀匹配,会包含隐藏目录(
.开头路径)下的文件——若需排除,
设置setMaxFileSize()(走filePathNotContains("/.")过滤)或自行过滤结果
FilePicker.with(this)
.setSelectType(FilePickerSelectType.ofMarkdown()) // .md / .markdown
// .setSelectType(FilePickerSelectType.ofXmind()) // .xmind
// .setSelectType(FilePickerSelectType.ofFreemind()) // .mm
.start()这三个类型只按文件名后缀查询与识别,不走 mime 轨,因为 MediaStore 给不出可靠的 mime:
| 后缀 | MediaStore 常见 mime | 后果(若按 mime 判) |
|---|---|---|
.xmind |
application/zip(xmind 本质是 zip 容器) |
查询会捞出所有 zip;图标显示成 zip |
.md |
text/plain 或 application/octet-stream |
图标显示成 txt |
.mm |
application/octet-stream |
无专属图标 |
因此它们不属于压缩包:不参与 ZIP_ALL、isArchive() 的任何推导,与.xmind 是 zip 容器这件事无关。
图标映射里这三个判断排在 isTxt() / isZip() 之前,后缀命中即显示专属图标。
归属上它们是文档类型,但
ofDocument()的查询条件不含这三种(ofDocument()行为保持不变),
需要单独用ofMarkdown()/ofXmind()/ofFreemind()。
后缀表统一登记在 FilePickerSelectType.DOC_EXTS,新增别名(如 mdown / mkd)只需加一行,查询与图标自动跟上。
tar/gz/bz2/iso/br/lz4/zstd/xz 等其他压缩格式不再单独提供 API,统一用
FilePickerSelectType.ofExtensions() 按任意后缀组合:
// 按任意后缀过滤,直接作为 selectType 传入
FilePicker.with(this)
.setSelectType(FilePickerSelectType.ofExtensions("xz", "tar", "bak"))
.start()-
后缀自动去掉前导点、转小写、去重,并过滤 SQL 通配符(
%_) -
任意后缀都能查(真机已验证:编造后缀
.qqqxx7、大写.ABCxyz、双重点.tar.gz均命中),
前提是文件位于 MediaStore 索引的公共存储;应用私有目录、.nomedia目录内的文件查不到 -
图标识别不受影响:内置的 tar/gz/bz2/iso/br/lz4/zstd/xz 类型常量与图标映射仍然生效,
老代码直接传FilePickerSelectType.TAR等字符串常量依旧可用 -
查询、识别由内部「压缩包登记处」统一驱动:
ZIP_ALL由登记处派生,
单个类型(如ZIP)只取登记处中自己那一行,互不影响
7.0.0 旧 API 迁移对照表:
| 6.x(已删除) | 7.0.0 等价写法 |
|---|---|
ofTar() |
FilePickerSelectType.ofExtensions("tar") |
ofGz() |
FilePickerSelectType.ofExtensions("gz", "tgz") |
ofBz2() |
FilePickerSelectType.ofExtensions("bz2") |
ofIso() |
FilePickerSelectType.ofExtensions("iso") |
ofBr() |
FilePickerSelectType.ofExtensions("br") |
ofLz4() |
FilePickerSelectType.ofExtensions("lz4") |
ofZstd() |
FilePickerSelectType.ofExtensions("zstd") |
ofXz() |
FilePickerSelectType.ofExtensions("xz") |
ofTar() + ofIso() 组合 |
FilePickerSelectType.ofExtensions("tar", "iso") |
也可以不做任何改动:把 selectType 直接传
FilePickerSelectType.TAR等字符串常量,查询、过滤、图标展示行为与旧版一致。
| 方法名 | 描述 |
|---|---|
setSelectType(type) |
设置选择的文件类型 |
setMaxSelectNumber(num) |
设置最大选择数量(0/Int.MAX_VALUE = 不限)。只在不用 setSelectRules 时生效;规则模式下合计上限由规则自动叠加,本方法被忽略 |
setSelectRules(vararg rules) |
按类型分别限量,见「按类型限量」 |
setExclusive(exclusive) |
互斥:图片与视频只能选其中一类。需先配 setSelectRules,否则不生效 |
setGifCountsAsImage(counts) |
GIF 是否占用图片额度,默认 true。需先配 setSelectRules,否则不生效 |
setMaxFileSize(size) |
设置最大文件大小(字节) |
setMinFileSize(size) |
设置最小文件大小(字节) |
setSlideChooseEnable(enable) |
是否启用滑动选择,默认 true |
setSingleClickEnable(enable) |
是否启用单选立即返回,默认 false。仅在 maxSelectNumber 为1时生效 |
setShowCameraEntry(enable) |
是否显示网格第一格的拍照/录像入口,默认 false。仅对单一的图片或视频类型生效,详见下文 |
setSelectedList(list) |
设置已选中的 MediaEntity 列表 |
setSelectedPathList(list) |
设置已选中的文件路径列表 |
setUiConfig(config) |
设置自定义UI配置 |
setOnSelectCallback(callback) |
设置选择结果回调 |
start() |
启动选择器 |
通过 FilePicker.with(this).setUiConfig(uiConfig) 进行设置。
val uiConfig = FilePickerUIConfig().apply {
isHideSelectTab = false
confirmBtnText = "完成"
allAlbumName = "所有文件"
// ... 更多配置
}| 属性 | 类型 | 描述 | 默认值 |
|---|---|---|---|
isHideSelectTab |
Boolean |
是否隐藏顶部分类Tab | false |
confirmBtnText |
String |
确定按钮的文本 | "确定" |
isPreviewPageIndexMode |
Boolean |
预览页面是否显示页码 | true |
allAlbumName |
String |
“全部”文件夹的显示名称 | "全部" |
previewText |
String |
预览页面的标题 | "预览" |
isShowBottomPreviewText |
Boolean |
是否显示底部预览按钮 | true |
previewSelectText |
String |
预览页面底部“选择”按钮文本 | "选择" |
originalText |
String |
“原图”选项的文本 | "原图" |
isShowOriginal |
Boolean |
是否显示“原图”选项 | true |
isOriginalChecked |
Boolean |
“原图”选项是否默认选中 | false |
isShowHomePageSelectedBottomListWidget |
Boolean |
是否显示主页底部已选列表 | true |
isShowSelectedListDeleteIcon |
Boolean |
是否显示已选列表项的删除按钮 | false |
selectedListDeleteIconResId |
Int |
已选列表项删除按钮的图标资源ID | 0 |
selectedListDeleteIconBackgroundColor |
Int |
已选列表项删除按钮的背景色 | Color.TRANSPARENT |
atLeastSelectOneToastContent |
String |
未选择任何文件时的提示 | "至少选择一个" |
selectMaxNumberOverToastContent |
String |
超出最大选择数量时的提示 | "已达到最大选择数量" |
selectTypeMaxToastContent |
String |
命中分类型上限时的提示(%1$s=类型名,%2$d=上限) |
"%1$s最多选择%2$d个" |
selectExclusiveConflictToastContent |
String |
互斥下点了另一类时的提示(%1$s=当前只能选的类型) |
"当前只能选择%1$s" |
selectRuleNotSatisfiedToastContent |
String |
必选未满足时的提示(%1$s=类型名,%2$d=要求数量) |
"%1$s必须选%2$d个" |
cameraEntryIconResId |
Int |
拍照入口的图标(0 = 库内默认) | 0 |
cameraEntryVideoIconResId |
Int |
录像入口的图标(0 = 库内默认) | 0 |
cameraEntryPhotoText |
String |
「拍照」半边下面的文字 | "拍照" |
cameraEntryVideoText |
String |
「录像」半边下面的文字 | "录像" |
cameraEntryFailedToastContent |
String |
相机入口启动失败时的提示 | "无法启动相机" |
cameraEntrySizeLimitToastContent |
String |
拍摄产物不符合大小限制时的提示 | "拍摄的文件不符合大小限制,未加入列表" |
folderNickNameMap |
HashMap<String, String> |
文件夹名称映射,用于自定义显示名称 | emptyHashMap() |
## !!!温馨提示: 如果UI效果与你的需求差距较大,强烈建议下载源码,自己修改一下。fork代码,自己改。!!!
FilePicker 旨在提供简洁、灵活、可定制的文件选择功能,广泛适用于相册、文件管理、视频选择等场景。
如需深入定制或遇到问题,欢迎补充需求,我可以帮您生成对应的开发指导。
本项目在开发过程中参考和使用了以下优秀的开源项目,特此致谢:
-
xwidget - 提供了超级方便的基础组件。
-
BRV —— 便捷的 RecyclerView 适配器库
-
BasePopup —— 强大的弹窗库
-
Glide —— 高效的图片加载库
-
androidx.media3 —— 官方视频播放组件
感谢你们的无私奉献,让开发变得更加高效和有趣!
在预览页直接编辑图片:画笔(涂鸦)/ 文字 / 裁剪 / 马赛克。 编辑结果不修改原图;点「完成」后把最终合并图保存到系统相册
Pictures/filepicker/, 返回给调用方的就是编辑后的真实图片地址。
编辑入口默认关闭,需要显式打开:
FilePicker.with(this)
.setEditInPreviewEnable(true) // ← 开关:预览页是否出现「编辑」入口
.start()开启后:
- 单图预览页右上角出现「编辑」
- 多选预览页(首页选完 N 张 → 预览)同样出现「编辑」,可逐张编辑
FilePicker.with(this)
.setSelectType(FilePickerSelectType.ofImage())
.setMaxSelectNumber(9)
// 1) 预览页可编辑(默认 false,必须显式打开)
.setEditInPreviewEnable(true)
// 2) 裁剪参数(可选)
.setCropAspectRatio(0f) // 0 = 自由裁剪;>0 = 固定比例(1f = 头像 1:1)
.setCropOutputMaxSize(2048) // 裁剪导出最长边,默认 2048
.setCropQuality(90) // 导出质量 60~100,默认 90
.setCropResetText("还原") // 裁剪页「还原」文案(多语言由接入方传入)
// 3) 编辑临时缓存目录(可选,默认 Android/data/<包名>/cache/filepicker/)
.setEditCacheDir(cacheDir.absolutePath)
.setOnSelectCallback { isUseOriginal, list ->
list.forEach { entity ->
// 编辑过的图:entity.editedPath 是编辑产物(会话内预览/角标用)
// 点确定返回时:path / uri 已经是编辑后的真实地址(Pictures/filepicker/xxx.jpg)
Log.d("picker", "原始: ${entity.path} 编辑产物: ${entity.editedPath}")
}
}
.start()| 字段 / API | 含义 |
|---|---|
MediaEntity.path / uri |
原图地址;从选择器返回结果时,编辑过的条目已换成编辑后的真实地址 |
MediaEntity.editedPath |
会话内编辑产物地址(预览、已编辑角标使用),返回结果时清空 |
FilePicker.clearEditCache(context) |
手动清理编辑临时缓存(返回调用方时库已自动异步清理) |
FilePicker.editCacheSize(context) |
当前编辑缓存大小(字节) |
FilePicker.editCacheDir(context) |
编辑缓存目录 |
- 编辑产物发布位置:
Pictures/filepicker/filepicker_edit_yyyyMMdd_HHmmss.jpg(系统相册可见,下次选择可直接再次选中) - 头像裁剪:
setCropAspectRatio(1f),单选模式下点图片直接进入 1:1 裁剪页
- 编辑是会话级覆盖:不改原图文件、不覆盖相册原图;「已编辑」角标显示在缩略图左下角(16dp)。
- 再次进入选择器并传入
setSelectedList(list)时,编辑过的条目以编辑后的图展示。 - Android 10+ 发布到相册走 MediaStore(无需存储权限);9 及以下写公共目录并触发媒体扫描。
- 文字/画笔比例相对原图短边:裁剪只裁图片,已有文字/画笔仍可编辑,越界部分自然裁掉。
头像场景:单选、点一张图直接进入 1:1 裁剪页(独立 Activity),裁剪确认后返回成品图。 不经过编辑弹窗,也不修改相册原图。
FilePicker.with(this)
.setSelectType(FilePickerSelectType.ofImage())
.setMaxSelectNumber(1) // 头像 = 单选(容量 1 时自动启用"单击即返回"逻辑)
.setCropAspectRatio(1f) // ← 关键:大于 0 时,单选模式点图直接进固定比例裁剪页
.setCropOutputMaxSize(1024) // 裁剪导出最长边(头像一般 512~1024 就够)
.setCropQuality(95) // 导出质量 60~100
.setOnSelectCallback { isUseOriginal, list ->
val avatar = list.firstOrNull()
// 返回时 path / uri 已经是裁剪后的成品地址(Pictures/filepicker/xxx.jpg)
Log.d("avatar", "裁剪结果: ${avatar?.path}")
}
.start()也可以保留勾选流程:
FilePicker.with(this)
.setMaxSelectNumber(1)
.setSingleClickEnable(true) // 允许"点一下就直接选中/裁剪"
.setCropAspectRatio(1f)
.start()| 环节 | 说明 |
|---|---|
| 入口 | 单选模式(容量=1)下点图片 → FilePickerCropActivity(1:1 固定裁剪框) |
| 裁剪页 UI | 取消 / 旋转 / 镜像 / 还原 / 确认裁剪;固定比例不可拖成其它比例 |
| 确认裁剪 | 生成成品图 → 发布到系统相册 Pictures/filepicker/ → 登记媒体库 |
| 返回结果 | MediaEntity.path / uri = 裁剪后的成品地址;editedPath 仅会话内使用 |
| 取消 | 关闭裁剪页,不产生任何文件 |
setCropAspectRatio(1f)只影响单选单击(头像)流程;多选场景的裁剪仍走 「预览 → 编辑 → 裁剪」(自由裁剪,受setEditInPreviewEnable(true)控制)。- 想用别的头像比例(如 4:3、16:9)直接传对应小数:
setCropAspectRatio(4f/3f)。 - 需要"先展示裁剪页再确认"的 UI,也可以按
FilePickerCropActivity.start(...)直接启动。
新增图片编辑器:画笔 / 文字 / 裁剪 / 马赛克,以及编辑结果的全链路管理。
- 编辑入口:单图预览页「编辑」、多选预览页「编辑」(
setEditInPreviewEnable(true)开启)。 编辑结果是会话级覆盖:原图文件不动,MediaEntity.editedPath指向编辑产物。 - 画笔(涂鸦):像素级涂抹,8 色可选、橡皮擦;面板增加锥形粗细滑条(自定义 View,左细右粗)。
- 文字:默认「字体色」模式(无背景)+ 「圆角背景」模式两种样式;8 色; 文字可拖动、点选后双指缩放;字号按图片短边比例存储,随图片缩放/裁剪保持一致。
- 裁剪:专注裁剪模式(隐藏主工具条,只留 旋转 / 镜像 / 还原 + ✕ / ✓); 旋转、镜像以裁剪框中心为轴;裁剪框限制在安全区域内(左右各 22dp、顶部状态栏下方、 底部裁剪菜单上方);裁剪只裁图片,已有文字/画笔仍可编辑、越界部分自然裁掉; 停手 2 秒自动放大裁剪框。
- 马赛克:像素马赛克 / 雾化两种效果,共用可调的笔刷粗细(比画笔更粗)。
- 已编辑角标:16dp,显示在图库网格、底部已选缩略图条、预览底条及 demo 结果格左下角(图片区域内)。
- 编辑产物发布:点「完成」合并出图后,复制到系统相册
Pictures/filepicker/并在媒体库登记 (Android 10+ 走 MediaStore,无需额外权限;9 及以下写公共目录 +MediaScannerConnection扫描), 返回给调用方的就是编辑后的真实地址,下次选择可直接再次选中。 - 缓存清理:返回调用方时异步清理编辑临时缓存(
clearCacheExcept保留结果文件),Pictures/filepicker/成品永久保留。 - 新增 API:
setCropAspectRatio/setCropOutputMaxSize/setCropQuality/setEditInPreviewEnable/setEditCacheDir/setCropResetText;FilePicker.clearEditCache/editCacheSize/editCacheDir。
- 文字/画笔比例统一相对原图短边:裁剪不再改变字号和笔刷粗细,画布与导出一致。
- 返回键:裁剪模式下先退出裁剪模式,再按才关闭编辑弹窗;文字输入先收键盘。
- 添加文字后立即显示(不点图片也可见);文字层级高于画笔与马赛克。
- 再次选择时带入的已编辑图片,缩略图/预览/再次编辑都用编辑后的图。
- demo:结果列表点击预览显示编辑后的图片。
修复预览页的一批体验问题:底部缩略图绿框复用残留、翻页手感、大图预览卡顿。
- 底部缩略图绿框残留:以前是"记住上一项、只清它一个",而翻页只改可见项, ViewHolder 被复用后会把绿框带到别的格子上。现在按 adapter position 整条重绘, 任何时候只有当前页那一格亮。
- 底条滚动:预览页 / 最终预览页 / 相册页统一改用
RecyclerView.smoothScrollToPosition(), 删掉手算居中坐标的 CenterSmoothScroller(findViewByPosition取不到视图时会算错落点,表现为"乱跳")。 - 底条间距:三处底部条都按"一屏露出 5 个半格子"压缩间距(只动间距,图片尺寸、 右上角删除按钮的位置和大小都不变);格子已完全可见时不再起无意义的平滑滚动动画。
- 大图预览卡顿:预览大图改为按屏幕 2 倍采样解码(原先是整张原图全尺寸解码, 4000×3000 的照片单张约 48MB),并先用网格缓存缩略图顶帧、预加载左右相邻页。 左右滑动明显跟手,屏幕内观感与原图一致。
- 无 API 变更,从 7.4.0 升级无需改代码。
明确新模型的启用条件:只有配置了
setSelectRules(...),setExclusive/setGifCountsAsImage才生效;一个规则都不配时,一律回退到setMaxSelectNumber。
-
新模型启用条件收敛为"规则表非空":
- 配了规则 → 分类型限量 + 互斥 + GIF 归并生效;合计上限由各类型
max自动叠加,setMaxSelectNumber被忽略(打 warn 日志); - 没配规则 →
setExclusive/setGifCountsAsImage被忽略(打 warn 日志), 数量限制仍由setMaxSelectNumber决定。
修掉了一个隐患:以前"只开互斥、不配规则"也会切到新模型,导致用户设的
maxSelectNumber被静默丢掉。 - 配了规则 → 分类型限量 + 互斥 + GIF 归并生效;合计上限由各类型
-
新增
SelectRulesEngine.createOrNull(...)作为启用判定的唯一入口(纯函数,可单测)。 -
demo:规则模式选了但没填图片/视频数量时,「最大选择数量」重新显示并生效,同时 Toast 提示。
-
单测补 2 条(启用条件 / 规则推导容量),共 30 条。
7.3.0 / 7.3.1 中间语义已被本版取代:7.3.0 是"合计上限与规则同时生效", 7.3.1 是"规则模式忽略 maxSelectNumber"。请以 7.4.0 为准。
数量限制只留一个来源:规则模式下
setMaxSelectNumber不再生效,合计上限由各类型max自动叠加得出。这样不存在"合计上限与规则要求冲突"的配置,也就不需要任何运行时抉择。
- 规则模式下
setMaxSelectNumber被忽略(打一条 warn 日志):合计上限 = 各类型max之和; 互斥模式下 = 单类型最大的max。调用方不需要也不应该再写第二个数字。 - 不调用
setSelectRules/setExclusive/setGifCountsAsImage时,setMaxSelectNumber依然是唯一口径,行为与老版本完全一致。 - 由于合计上限恒 ≥ 必选数量,"配置矛盾"在库内已经不可能发生(引擎里的
checkSatisfiable仅作为直接构造SelectRulesEngine时的兜底)。 - demo:规则模式为「无」时显示「最大选择数量」并隐藏分类型输入;选共存 / 互斥时反过来。
合并重复 API:
setTotalMaxCount已移除,合计上限统一用setMaxSelectNumber。 7.2.0 只发布了 1 天、该 API 没有实际使用;如果你的项目已经用了,换成setMaxSelectNumber(n)即可。
-
数量限制收敛为两个维度、两个 API:
- 合计上限 →
setMaxSelectNumber(n)(一直有效,两个模型都认); - 分类型 min/max →
setSelectRules(...)。
两者同时生效、取交集,不再有"启用规则后
setMaxSelectNumber被忽略"这条规则 —— 那套"单一数据源" 的约定,实际效果是给同一个概念(合计上限)造了第二个名字,对使用者不友好。 - 合计上限 →
-
移除
setTotalMaxCount:它和setMaxSelectNumber含义完全相同。需要合计上限直接用setMaxSelectNumber(n);不设置时,合计容量仍由各类型max之和自动推导。 -
示例同步更新:
setSelectRules(Image(0, 9), Video(0, 1)).setMaxSelectNumber(9)表示 "图片 ≤ 9、视频 ≤ 1,且合计 ≤ 9"。
没有破坏性 API 变更:未删除方法、未修改签名。不调用新增的
setSelectRules/setExclusive/setTotalMaxCount/setGifCountsAsImage时,行为与 7.1.0 完全一致。
- 按类型分别限量:
setSelectRules(SelectRule...),图片 / 视频 / GIF 各自设置min/max。max是硬上限,min > 0表示"必须选"。命中上限时提示的是具体类型的额度 ("图片最多选择2个"),不再笼统地报"已达到最大选择数量" —— 此时视频可能还有额度, 笼统文案会让用户误以为整体已满。 - 新类型
SelectRule:ofImage(min, max)/ofVideo(min, max)/ofGif(min, max); 带encode/decode(intent 传递用),配置非法(如min > max)在构造时就报错。 - 互斥选择:
setExclusive(true),图片和视频只能选其中一类。只有一类配了min > 0时直接锁它 (等于只能选图片 / 只能选视频);两类都配了min > 0时先点先锁、另一类的必选本次作废; 选中的类型全部取消后自动解锁。 - 合计上限:
setTotalMaxCount(count),属于新模型的一部分(不传则由各类型max之和推导)。 - GIF 额度归属:默认 GIF 占用"图片"额度;单独配了
SelectRule.ofGif时用自己的规则; 可用setGifCountsAsImage(false)关掉归并。 FilePickerUIConfig新增 3 个提示文案:selectTypeMaxToastContent、selectExclusiveConflictToastContent、selectRuleNotSatisfiedToastContent,可自定义 / 多语言。- 必选不满足时确定按钮不高亮:按钮置灰(alpha 0.4)但仍可点击,点击吐司"图片必须选3个", 不会返回 —— 既不让用户误以为能提交,也不会出现"按钮点不动又不知道为什么"。
- 滑动连选按类型配额筛选:一次划过很多格时按顺序贪心填充,不会先超再回退。
- 配置矛盾在开发期就报错:合计容量装不下必选数量(如合计 1 却要求图片必须 9 个)时,
start()直接抛IllegalArgumentException,不留到运行期让用户卡在"确定永远点不了"。 FilePickerLog.w():新增 warn 级别日志,排查配置冲突用。- demo 新增"规则模式"入口:首页可选
无 / 共存 / 互斥,再填图片数量、视频数量并勾选"强制", demo 会按这三个参数自动拼出对应的SelectRule。
- 同时调用
setMaxSelectNumber和新模型方法时,setMaxSelectNumber被忽略并打 warn 日志: 数量限制只保留一个数据源,避免两个数字互相打架。 - 预选列表(
setSelectedList)超出规则时按规则截断并打 warn 日志。 setSingleClickEnable的生效条件从"maxSelectNumber == 1"改为"总容量 == 1" (总容量 =setTotalMaxCount,未设置时由各类型max之和推导), 因此SelectRule.ofVideo(1, 1)也能触发"点一下就返回"。不启用新模型时二者等价,行为不变。
- 26 条 JVM 单测:
SelectRulesEngineTest(18 条:共存 / 互斥 / 必选 / 上限 / GIF 归并 / 批量筛选 / 矛盾配置 / 序列化)+SelectMessagesTest(8 条:提示文案映射)。 - 真机验证(华为 BRE-AL80 / Android 12):互斥锁定、分类型上限、必选拦截、文案、老 API 行为不变、 两套参数并存时让位。
本版没有破坏性 API 变更:未删除任何方法、未修改任何签名,老代码升级后可直接编译。 只有两处
equals/hashCode的语义变了,见末尾「行为变更」。
-
拍照 / 录像入口(默认关闭):
setShowCameraEntry(true)在网格第一格放一个相机格子,
点击调起系统相机拍照或录像,产物写进系统相册(DCIM/Camera)并自动插到列表第一个。
默认false,不开启时对宿主零影响 —— 不挂 header、不注册 viewType、不碰权限、
不给宿主 manifest 加任何声明(无<provider>、无<queries>)。- 画法随"当前类型下相机能产出什么"变:只有图片 / 只有视频 → 整格一个按钮(图标 + 文字);
图片与视频都有(ofAll()/ofAllWithGif()的「全部」页)→ 从中间劈开,左半边录像、
右半边拍照,中间一条分隔线 - 有 tab 的类型里入口跟着当前 tab 走:在「视频」页只给录像、「图片」页只给拍照、「GIF」页不给
- 拍完回来默认勾上("这张就是要用的");已到
maxSelectNumber上限时只插入不勾选并给提示;
单选 +setSingleClickEnable(true)时拍完直接回调返回 - 产物大小不符合
minFileSize/maxFileSize时不插入、不勾选,并给提示 - 宿主声明了 CAMERA 却未授权时,库先申请权限再起相机
- 探相机用
startActivity包 try/catch,不用resolveActivity()—— targetSdk ≥ 30 且宿主
没声明<queries>时它对相机 action 一律返回null(实测),拿它当守卫会让入口永远点不动 - 详见「拍照 / 录像入口」一节
- 画法随"当前类型下相机能产出什么"变:只有图片 / 只有视频 → 整格一个按钮(图标 + 文字);
-
文档类型入口:
ofMarkdown()(.md/.markdown)、ofXmind()(.xmind)、ofFreemind()(.mm),
各有专属图标。三者只按后缀查询与识别(.xmind的 mime 是application/zip、.md可能是
text/plain,mime 轨不可靠),归在文档类型下、不属于压缩包:不参与ZIP_ALL/isArchive()推导。
后缀表见FilePickerSelectType.DOC_EXTS。ofDocument()的查询条件不含这三种,行为不变。 -
FilePickerUIConfig新增相机入口相关配置:cameraEntryIconResId、cameraEntryVideoIconResId、
cameraEntryPhotoText、cameraEntryVideoText、cameraEntrySizeLimitToastContent、
cameraEntryFailedToastContent—— 图标与文案全部可自定义,方便多语言适配。
-
缩略图与原图不同源("缩略图是这张、点开是另一张"):缩略图的权威来源改为真实文件,
系统预生成的缩略图只当首帧占位;缩略图与原图统一带MediaStoreSignature(mime + 修改时间 + 方向),
文件被覆盖/编辑后旧缓存立即失效、不会继续显示旧图。MediaEntity为此新增dateModified,
查询补上DATE_MODIFIED列。 -
预览页显示的不是点的那张:
ViewPager2.setCurrentItem内部会把负下标静默夹到第 0 页,
而列表在 fast→full 两次扫描之间会被整体替换,indexOf可能返回 -1。现在所有交给setCurrentItem
的下标都先过一道PreviewIndexResolver,规整到[0, size-1]。 -
ofAllWithGif()图片全丢:拼查询条件时,请求里同时含图片和 GIF、又不是"只有 GIF"时
(正是ofAllWithGif),图片那一支一个条件都没加上,最终只剩mime_type LIKE 'video/%',
图片被整片排除。已补上该分支。 -
视频预览里封面缩在中间:两个原因叠加 —— ① 只有视频面被按宽高比撑开,封面一直是
wrap_content+fitCenter,高度取决于缩略图自己的固有尺寸;② 宽高比没算旋转角:
竖着录的视频存储宽高是横的(如 3840×2160),真实方向在orientation/unappliedRotationDegrees里,
而封面缩略图是系统已经旋转好的竖图,两者对不上就被按高度压小。现在两者共用同一个盒子,
并且都按显示方向算比例。 -
拍摄产物被当成"没拍到"丢掉:原先用
MediaStore的SIZE <= 0判断有没有内容,
但个别机型SIZE回填滞后,会把刚拍的照片丢掉。现在SIZE为 0 时再真读一个字节确认;
另外"查询失败"不再等同于"没有内容" —— 那条路径不删任何东西。 -
ImagePreviewActivity的 url 判空写反了:正常地址会被当成非法地址直接 toast 并结束。
这两处原来都是错的,修完之后语义变了 —— 如果宿主曾绕过它们,升级后行为会跟着变:
-
MediaEntity.equals/hashCode:原来只比path,两个都拿不到 path 时会被判成同一个实体,
于是indexOf/contains会命中错的元素(典型表现就是预览错位)。现在 path 无效时退回比较uri,
两者都没有有效标识则一律不相等。 -
MediaFolder.equals:原来三个字段的判断写反了(相等时返回false),除了同一个引用以外
永远不相等,mediaFolders.remove(folder)/contains之类的操作全部失效。现在按
name + folderPath + mediaEntityList 正常比较。
-
类型入口 API 收敛到
FilePickerSelectType:ofAll()/ofImage()/ofVideo()/ofZipAll()等
全部类型方法现在挂在FilePickerSelectType上(语义更贴合)。FilePicker.ofXxx()系列标注
@Deprecated但保留且可用(内部转发,行为不变),IDE 会提示迁移。 -
API 精简(破坏性变更):压缩包相关入口只保留核心四个
ofZipAll()/ofZip()/ofRar()/of7Z(),
删除ofTar()/ofGz()/ofBz2()/ofIso()/ofBr()/ofLz4()/ofZstd()/ofXz()。 -
新增自定义后缀过滤:任意后缀组合不再需要为每种类型加 API,一行搞定:
FilePicker.with(this) .setSelectType(FilePickerSelectType.ofExtensions("xz", "tar", "bak")) .start()
-
后缀自动归一化:去前导点、转小写、去重、过滤 SQL 通配符(
%_) -
过滤按文件名后缀匹配(不区分大小写),mime 轨不参与(认不出的后缀在 MediaStore 里多为
application/octet-stream) -
SelectTypeUtil.isValidType()已放行ext:前缀的自定义类型,UI 自动按"文件列表"模式呈现
-
-
兼容性说明:
FilePickerSelectType.TAR/GZ/BZ2/ISO/BR/LZ4/ZSTD/XZ等类型常量
全部保留,老代码直接传字符串常量依旧可用,行为不变;图标识别逻辑不受本次精简影响。 -
压缩包逻辑收敛到唯一登记处:类型 → 后缀(
ARCHIVE_EXTS)与类型 → mime(ARCHIVE_MIMES)
两张登记表是唯一数据源,ZIP_ALL、MediaStore 查询条件、MediaEntity类型识别(isArchive()/isZip()等)
全部由其派生。新增压缩格式只需在两张表里各加一行,查询/识别/图标自动跟上,
不会再出现某处手抄漏掉新格式的问题。 -
新增接入建议:有专属类型入口的后缀(图片/音频/文档/APK/核心压缩包)优先用
ofXxx()入口,
ofExtensions()用于无对应类型的任意后缀。两者差异见「类型入口怎么选」一节。 -
修复:RAR 文件识别不到图标/查询不到。
不同设备的 MimeTypeMap 会把.rar映射成三种 mime 之一(application/x-rar-compressed、
application/vnd.rar、application/rar),现在三种全部兼容;查询与图标识别同时增加文件名后缀兜底。 -
修复:MediaStore 通用类型(
application/octet-stream)文件识别不到。
mime 为空或为 octet-stream 时,按文件名后缀兜底识别(rar/7z/tar/gz/bz2/iso/br/lz4/zstd/xz、
图片/视频/音频/文档等全部受益),正确标注了 mime 的文件行为不变。 -
修复:
pptx/xlsx/docx图标映射错误(原来落到错误图标),rar使用专属图标,
新增 zip/tar/gz/iso/rar/7z 等压缩格式独立图标;isArchive()补齐bz2覆盖。 -
去除硬编码:媒体类型判断中的 MIME 字符串全部收敛到
FilePickerMimeType常量。
-
性能大幅优化:图片数量很多的场景下,打开选择器的加载速度显著提升。
-
媒体库扫描去掉逐行
File.stat()与逐行日志,列索引一次性提取。 -
选择状态由「遍历查找」改为「路径 → 下标」哈希表,拖拽/滑动多选在千张级别也保持 O(1)。
-
首屏聚合、已选列表比对等重计算移出主线程,避免掉帧。
-
再次进入选择器时,数据已加载则不再全量重扫。
-
-
修复:设置最大选择数量后点击开始选择时偶发崩溃(
ConcurrentModificationException)。 -
修复:在部分华为等机型上启动报
ClassNotFoundException: FilePickerUIConfig的问题(UI 配置改为纯基础类型 Bundle 传递,旧方式保持兼容)。 -
修复:本地 GIF 不再写入磁盘缓存的问题,滑动加载更省流量/解码开销。
-
拖拽排序、滑动多选、单选/取消、预览等交互行为与 5.x 完全一致。
-
解决选择图片过多,intent超过1M的问题。随便选无上限
-
优化index数字过大展示不全的问题。目前支持到 99999 张
-
增加文件夹别名设置setUiConfig中folderNickNameMap里自行添加,默认不添加。也可做翻译文件夹用。仅文件夹哦。
-
其他的UI上的文字都在FilePickerUIConfig中有对应属性,请仔细查看文档。
-
修复最后一行选中图片被遮挡的问题
-
优化放大图标,更好看了。
- 优化过多图片首屏展示慢的问题。
- 修复音频播放放到后台还在继续的问题。
- 剔除日志影响
- 新版UI 点击更丝滑
-
适配底部小横导航栏
-
优化UI选择样式
-
新增:支持更多文件类型选择,如文档(audio, pdf, doc, ppt, excel, txt)、APK、各类压缩包等。
-
新增:
setSingleClickEnable(boolean)API,支持单选模式下单击立即返回。 -
新增:
setSlideChooseEnable(boolean)API,可禁用滑动选择手势。 -
新增:
FilePickerUIConfig中增加isHideSelectTab、allAlbumName、isShowBottomPreviewText等UI配置项。
- 修复maxFileSize默认值:目前改为Long.MAX_VALUE .
- 适配手机底部导航栏,为屏幕内虚拟三键的情况。
