-
使用最新版的 filepicker 库,必须使用最新版的xwidget库。都是 5.8.1 以上确保。
-
用了很多,都没有丝滑的选择效果的开源库,滑动选择丝滑。闲来没事,随手写一个吧。
-
图库选择,文件选择
-
仿华为相册滑动选择手势逻辑,
-
微信选择库样式风格
-
支持多选,限制数量,不限制数量。
-
使用场景,多用于 视频剪辑选择库,相册管理选择库。那去玩吧。
-
或者:点击下载 app-debug.apk
- 下面的这个三方库,本maven仓库中的aar都过滤掉了。建议自己从下面引用,防止库冲突。
- **filepicker
** - **xwidget
** - **BRV
** - **BasePopup
** - androidx.media3 这里建议使用1.11.0
稳定版 - glide
dependencies {
// 基础组件库 (必须)
implementation("com.gitee.pichs:filepicker:7.1.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.1.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。
| 方法名 | 描述 |
|---|---|
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) |
设置最大选择数量 |
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 |
超出最大选择数量时的提示 | "已达到最大选择数量" |
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 —— 官方视频播放组件
感谢你们的无私奉献,让开发变得更加高效和有趣!
本版没有破坏性 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 .
- 适配手机底部导航栏,为屏幕内虚拟三键的情况。
