FileOperator
- 🚀FileOperator GitHub
- 🚀更简单的处理Android系统文件操作
- 🚀适用于 Android 4.4 及以上系统 , 兼容AndroidQ新的存储策略
- 🚀图片压缩算法参考 Luban
- 🚀Kotlin 案例 👉 app
- 🚀Java 案例 👉 sample_java
Gradle:
Project build.gradle
:
repositories {
maven { url 'https://dl.bintray.com/javakam/maven' }
}
推荐方式 :
implementation 'ando.file:core:1.0.0' //核心库必选
implementation 'ando.file:android-q:1.0.0' //AndroidQ & Android 11 兼容库
implementation 'ando.file:compressor:1.0.0' //图片压缩,核心算法采用 Luban
implementation 'ando.file:selector:1.0.0' //文件选择器
整体引入(不推荐):
implementation 'ando.file:FileOperator:0.9.1'
Application
中初始化:
FileOperator.init(this,BuildConfig.DEBUG)
演示
功能列表 | 缓存目录 |
---|---|
API
App Specific | MediaStore | Storage Access Framework |
---|---|---|
文件选择
单图 + 压缩 | 多图 + 压缩 | 多文件 |
---|---|---|
Usage:
1. 单选图片
val optionsImage = FileSelectOptions()
optionsImage.fileType = FileType.IMAGE
options.mMinCount = 0
options.mMaxCount = 10
optionsImage.mSingleFileMaxSize = 2097152 // 20M = 20971520 B
optionsImage.mSingleFileMaxSizeTip = "图片最大不超过2M!"
optionsImage.mAllFilesMaxSize = 5242880 //5M 5242880 ; 20M = 20971520 B
optionsImage.mAllFilesMaxSizeTip = "总图片大小不超过5M!"
optionsImage.mFileCondition = object : FileSelectCondition {
override fun accept(fileType: FileType, uri: Uri?): Boolean {
return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
}
}
mFileSelector = FileSelector
.with(this)
.setRequestCode(REQUEST_CHOOSE_FILE)
.setSelectMode(false)
.setMinCount(1, "至少选一个文件!")
.setMaxCount(10, "最多选十个文件!")
.setSingleFileMaxSize(5242880, "大小不能超过5M!") //5M 5242880 ; 100M = 104857600 KB
.setAllFilesMaxSize(10485760, "总大小不能超过10M!")
.setMimeTypes(MIME_MEDIA)//默认全部文件, 不同 arrayOf("video/*","audio/*","image/*") 系统提供的选择UI不一样
.applyOptions(optionsImage)
//优先使用 FileOptions 中设置的 FileSelectCondition
.filter(object : FileSelectCondition {
override fun accept(fileType: FileType, uri: Uri?): Boolean {
return when (fileType) {
FileType.IMAGE -> {
return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
}
FileType.VIDEO -> true
FileType.AUDIO -> true
else -> true
}
}
})
.callback(object : FileSelectCallBack {
override fun onSuccess(results: List<FileSelectResult>?) {
FileLogger.w("回调 onSuccess ${results?.size}")
mTvResult.text = ""
if (results.isNullOrEmpty()) return
shortToast("正在压缩图片...")
showSelectResult(results)
}
override fun onError(e: Throwable?) {
FileLogger.e("回调 onError ${e?.message}")
mTvResultError.text = mTvResultError.text.toString().plus(" 错误信息: ${e?.message} \n")
}
})
.choose()
2. 多选图片
val optionsImage = FileSelectOptions()
optionsImage.fileType = FileType.IMAGE
options.mMinCount = 0
options.mMaxCount = 10
optionsImage.mSingleFileMaxSize = 3145728 // 20M = 20971520 B
optionsImage.mSingleFileMaxSizeTip = "单张图片最大不超过3M!"
optionsImage.mAllFilesMaxSize = 5242880 //3M 3145728 ; 5M 5242880 ; 10M 10485760 ; 20M = 20971520 B
optionsImage.mAllFilesMaxSizeTip = "图片总大小不超过5M!"
optionsImage.mFileCondition = object : FileSelectCondition {
override fun accept(fileType: FileType, uri: Uri?): Boolean {
return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
}
}
mFileSelector = FileSelector
.with(this)
.setRequestCode(REQUEST_CHOOSE_FILE)
.setSelectMode(true)
.setMinCount(1, "至少选一个文件!")
.setMaxCount(10, "最多选十个文件!")
//优先以自定义的 optionsImage.mSingleFileMaxSize 为准5M 5242880 ; 100M = 104857600 KB
.setSingleFileMaxSize(2097152, "大小不能超过2M!")
.setAllFilesMaxSize(20971520, "总文件大小不能超过20M!")
//1.OVER_SIZE_LIMIT_ALL_DONT 超过限制大小全部不返回 ;2.OVER_SIZE_LIMIT_EXCEPT_OVERFLOW_PART 超过限制大小去掉后面相同类型文件
.setOverSizeLimitStrategy(this.mOverSizeStrategy)
.setMimeTypes(MIME_MEDIA)//默认全部文件, 不同 arrayOf("video/*","audio/*","image/*") 系统提供的选择UI不一样
.applyOptions(optionsImage)
//优先使用 FileOptions 中设置的 FileSelectCondition
.filter(object : FileSelectCondition {
override fun accept(fileType: FileType, uri: Uri?): Boolean {
return when (fileType) {
FileType.IMAGE -> {
return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
}
FileType.VIDEO -> true
FileType.AUDIO -> true
else -> true
}
}
})
.callback(object : FileSelectCallBack {
override fun onSuccess(results: List<FileSelectResult>?) {
FileLogger.w("回调 onSuccess ${results?.size}")
mTvResult.text = ""
if (results.isNullOrEmpty()) return
shortToast("正在压缩图片...")
showSelectResult(results)
}
override fun onError(e: Throwable?) {
FileLogger.e("回调 onError ${e?.message}")
mTvResultError.text = mTvResultError.text.toString().plus(" 错误信息: ${e?.message} \n")
}
})
.choose()
3. 多选文件
🌴适用于处理复杂文件选择情形, 如: 选取图片、视频文件,其中图片至少选择一张, 最多选择两张, 每张图片大小不超过3M, 全部图片大小不超过5M ; 视频文件只能选择一个, 每个视频大小不超过20M, 全部视频大小不超过30M 。
//图片
val optionsImage = FileSelectOptions().apply {
fileType = FileType.IMAGE
mMinCount = 1
mMaxCount = 2
mMinCountTip = "至少选择一张图片"
mMaxCountTip = "最多选择两张图片"
mSingleFileMaxSize = 3145728 // 20M = 20971520 B
mSingleFileMaxSizeTip = "单张图片最大不超过3M!"
mAllFilesMaxSize = 5242880 // 5M 5242880
mAllFilesMaxSizeTip = "图片总大小不超过5M!"
mFileCondition = object : FileSelectCondition {
override fun accept(fileType: FileType, uri: Uri?): Boolean {
return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
}
}
}
//视频
val optionsVideo = FileSelectOptions().apply {
fileType = FileType.VIDEO
mMinCount = 1
mMaxCount = 1
mMinCountTip = "至少选择一个视频文件"
mMaxCountTip = "最多选择一个视频文件"
mSingleFileMaxSize = 20971520 // 20M = 20971520 B
mSingleFileMaxSizeTip = "单视频最大不超过20M!"
mAllFilesMaxSize = 31457280 //3M 3145728
mAllFilesMaxSizeTip = "视频总大小不超过30M!"
mFileCondition = object : FileSelectCondition {
override fun accept(fileType: FileType, uri: Uri?): Boolean {
return (uri != null)
}
}
}
mFileSelector = FileSelector
.with(this)
.setRequestCode(REQUEST_CHOOSE_FILE)
.setSelectMode(true)
.setMinCount(1, "至少选一个文件!")
.setMaxCount(5, "最多选五个文件!")
// 优先使用自定义 FileSelectOptions 中设置的单文件大小限制,如果没有设置则采用该值
// 100M = 104857600 KB ;80M 83886080 ;50M 52428800 ; 20M 20971520 ;5M 5242880 ;
.setSingleFileMaxSize(2097152, "单文件大小不能超过2M!")
.setAllFilesMaxSize(52428800, "总文件大小不能超过50M!")
// 超过限制大小两种返回策略: 1.OVER_SIZE_LIMIT_ALL_DONT,超过限制大小全部不返回;2.OVER_SIZE_LIMIT_EXCEPT_OVERFLOW_PART,超过限制大小去掉后面相同类型文件
.setOverSizeLimitStrategy(OVER_SIZE_LIMIT_EXCEPT_OVERFLOW_PART)
.setMimeTypes(null)//默认为 null,*/* 即不做文件类型限定; MIME_MEDIA 媒体文件, 不同 arrayOf("video/*","audio/*","image/*") 系统提供的选择UI不一样
.applyOptions(optionsImage, optionsVideo)
// 优先使用 FileOptions 中设置的 FileSelectCondition , 没有的情况下才使用通用的
.filter(object : FileSelectCondition {
override fun accept(fileType: FileType, uri: Uri?): Boolean {
return when (fileType) {
FileType.IMAGE -> {
return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
}
FileType.VIDEO -> true
FileType.AUDIO -> true
else -> true
}
}
})
.callback(object : FileSelectCallBack {
override fun onSuccess(results: List<FileSelectResult>?) {
FileLogger.w("回调 onSuccess ${results?.size}")
mTvResult.text = ""
if (results.isNullOrEmpty()) return
showSelectResult(results)
}
override fun onError(e: Throwable?) {
FileLogger.e("回调 onError ${e?.message}")
mTvResultError.text = mTvResultError.text.toString().plus(" 错误信息: ${e?.message} \n")
}
})
.choose()
ImageCompressor.kt
4.压缩图片//T 为 String.filePath / Uri / File
fun <T> compressImage(photos: List<T>) {
ImageCompressor
.with(this)
.load(photos)
.ignoreBy(100)//B
.setTargetDir(getPathImageCache())
.setFocusAlpha(false)
.enableCache(true)
.filter(object : ImageCompressPredicate {
override fun apply(uri: Uri?): Boolean {
//getFilePathByUri(uri)
FileLogger.i("image predicate $uri ${getFilePathByUri(uri)}")
return if (uri != null) {
val path = getFilePathByUri(uri)
!(TextUtils.isEmpty(path) || (path?.toLowerCase()
?.endsWith(".gif") == true))
} else {
false
}
}
})
.setRenameListener(object : OnImageRenameListener {
override fun rename(uri: Uri?): String? {
try {
val filePath = getFilePathByUri(uri)
val md = MessageDigest.getInstance("MD5")
md.update(filePath?.toByteArray() ?: return "")
return BigInteger(1, md.digest()).toString(32)
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
}
return ""
}
})
.setImageCompressListener(object : OnImageCompressListener {
override fun onStart() {}
override fun onSuccess(uri: Uri?) {
val path = "$cacheDir/image/"
FileLogger.i("compress onSuccess uri=$uri path=${uri?.path} 缓存目录总大小=${FileSizeUtils.getFolderSize(File(path))}")
val bitmap = getBitmapFromUri(uri)
dumpMetaData(uri) { displayName: String?, size: String? ->
runOnUiThread {
mTvResult.text = mTvResult.text.toString().plus(
"\n ---------\n👉压缩后 \n Uri : $uri \n 路径: ${uri?.path} \n 文件名称 :$displayName \n 大小:$size B \n" +
"格式化 : ${FileSizeUtils.formatFileSize(size?.toLong() ?: 0L)}\n ---------"
)
}
}
mIvCompressed.setImageBitmap(bitmap)
}
override fun onError(e: Throwable?) {
FileLogger.e("compress onError ${e?.message}")
}
}).launch()
}
直接使用静态方法
FileMimeType.kt
1. 获取文件MimeType类型👉File Name/Path/Url
获取相应MimeType
根据fun getMimeType(str: String?): String {...}
fun getMimeType(uri: Uri?): String {...}
//MimeTypeMap.getSingleton().getMimeTypeFromExtension(...) 的补充
fun getMimeTypeSupplement(fileName: String): String {...}
FileSizeUtils.kt
2. 计算文件或文件夹的大小👉文件/文件夹
大小
获取指定@Throws(Exception::class)
fun getFolderSize(file: File?): Long {
var size = 0L
if (file == null || !file.exists()) return size
val files = file.listFiles()
if (files.isNullOrEmpty()) return size
for (i in files.indices) {
size += if (files[i].isDirectory) getFolderSize(files[i]) else getFileSize(files[i])
}
return size
}
获取文件大小
fun getFileSize(file: File?): Long{...}
fun getFileSize(uri: Uri?): Long{...}
文件/文件夹
大小
自动计算指定自动计算指定文件或指定文件夹的大小 , 返回值带 B、KB、M、GB、TB 单位的字符串
fun getFileOrDirSizeFormatted(path: String?): String {}...}
BigDecimal
实现)
格式化大小(//scale 表示 精确到小数点以后几位
fun formatFileSize(size: Long, scale: Int): String {...}
转换文件大小,指定转换的类型:
//scale 精确到小数点以后几位
fun formatSizeByType(size: Long, scale: Int, sizeType: FileSizeType): BigDecimal =
BigDecimal(size.toDouble()).divide(
BigDecimal(
when (sizeType) {
SIZE_TYPE_B -> 1L
SIZE_TYPE_KB -> 1024L
SIZE_TYPE_MB -> 1024L * 1024L
SIZE_TYPE_GB -> 1024L * 1024L * 1024L
SIZE_TYPE_TB -> 1024L * 1024L * 1024L * 1024L
}
),
scale,
if (sizeType == SIZE_TYPE_B) BigDecimal.ROUND_DOWN else BigDecimal.ROUND_HALF_UP
)
转换文件大小带单位:
fun getFormattedSizeByType(size: Long, scale: Int, sizeType: FileSizeType): String {
return "${formatSizeByType(size, scale, sizeType).toPlainString()}${sizeType.unit}"
}
FileOpener.kt
3. 直接打开Url/Uri(远程or本地)👉Url
对应的系统应用
直接打开eg: 如果url是视频地址,则直接用系统的播放器打开
fun openUrl(activity: Activity, url: String?) {
try {
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.parse(url), getMimeType(url))
activity.startActivity(intent)
} catch (e: Exception) {
FileLogger.e("openUrl error : " + e.message)
}
}
根据 文件路径 和 类型(后缀判断) 显示支持该格式的程序
fun openFileBySystemChooser(context: Any, uri: Uri?, mimeType: String? = null) =
uri?.let { u ->
Intent.createChooser(createOpenFileIntent(u, mimeType), "选择程序")?.let {
startActivity(context, it)
}
}
选择文件【调用系统的文件管理】
fun createChooseIntent(mimeType: String?, mimeTypes: Array<String>?, multiSelect: Boolean): Intent =
// Implicitly allow the user to select a particular kind of data. Same as : Intent.ACTION_GET_CONTENT
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiSelect)
// The MIME data type filter
//intent.setType("image/*"); //选择图片
//intent.setType("audio/*"); //选择音频
//intent.setType("video/*"); //选择视频 (mp4 3gp 是 android支持的视频格式)
//intent.setType("file/*"); //比 */* 少了一些侧边栏选项
//intent.setType("video/*;image/*");//错误方式;同时选择视频和图片 -> https://www.jianshu.com/p/e98c97669af0
if (mimeType.isNullOrBlank() && mimeTypes.isNullOrEmpty()) type = "*/*"
else {
type = if (mimeType.isNullOrEmpty()) "*/*" else mimeType
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
}
// Only return URIs that can be opened with ContentResolver
addCategory(Intent.CATEGORY_OPENABLE)
}
注:
1.Intent.setType 不能为空!
2.mimeTypes 会覆盖 mimeType
3.ACTION_GET_CONTENT , ACTION_OPEN_DOCUMENT 效果相同
4.开启多选 resultCode=-1
FileUri.kt
4. 获取文件Uri/Path👉File
路径中获取Uri
从fun getUriByPath(path: String?): Uri? = if (path.isNullOrBlank()) null else getUriByFile(File(path))
fun getUriByFile(file: File?): Uri? {
if (file == null) return null
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val authority = FileOperator.getContext().packageName + PATH_SUFFIX
FileProvider.getUriForFile(FileOperator.getContext(), authority, file)
} else {
Uri.fromFile(file)
}
}
Uri
对应的文件路径,兼容API 26
获取fun getFilePathByUri(context: Context?, uri: Uri?): String? {
if (context == null || uri == null) return null
val scheme = uri.scheme
// 以 file:// 开头的
if (ContentResolver.SCHEME_FILE.equals(scheme, ignoreCase = true)) {//使用第三方应用打开
uri.path
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { //4.4以后
getPath(context, uri)
} else { //4.4以下
getPathKitkat(context, uri)
}
}
FileUtils.kt
5. 通用文件工具类👉- getExtension 获取文件后缀
jpg
- getExtensionFull 获取文件后缀
.jpg
- getExtensionFromUri(uri: Uri?) 获取文件后缀
- deleteFile 删除文件或目录
- deleteFilesButDir(file: File?, vararg excludeDirs: String?) 删除文件或目录 ,
excludeDirs
跳过指定名称的一些目录/文件
- deleteFileDir 只删除文件,不删除文件夹
- readFileText 读取文本文件中的内容
String
- readFileBytes 读取文本文件中的内容
ByteArray
- copyFile 根据文件路径拷贝文件
java.nio
eg :boolean copyFile = FileUtils.copyFile(fileOld, "/test_" + i, getExternalFilesDir(null).getPath());
File fileNew =new File( getExternalFilesDir(null).getPath() +"/"+ "test_" + i);
- write2File(bitmap: Bitmap, fileName: String?)
- write2File(input: InputStream?, filePath: String?)
- isLocal 检验是否为本地URI
- isGif 检验是否为 gif
注意的点
-
onActivityResult
中要把选择文件的结果交给FileSelector
处理mFileSelector?.obtainResult(requestCode, resultCode, data)
-
选择文件不满足预设条件时,有两种策略 :
-
1.当设置总文件大小限制时,有两种策略 OVER_SIZE_LIMIT_ALL_DONT 只要有一个文件超出直接返回 onError
-
2.OVER_SIZE_LIMIT_EXCEPT_OVERFLOW_PART 去掉超过限制大小的溢出部分的文件
-
-
选择文件数据:单选 Intent.getData ; 多选 Intent.getClipData
-
Android 系统问题 : Intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) 开启多选条件下只选择一个文件时,需要安装单选逻辑走... Σ( ° △ °|||)︴
-
回调处理
多选模式下,建议使用统一的 CallBack 回调;
单选模式下,如果配置了自定义的 CallBack , 则优先使用该回调;否则使用统一的 CallBack
未来任务
1.做一个自定义UI的文件管理器
2.增加Fragment使用案例 , 视频压缩-郭笑醒 , 清除缓存功能 , 外置存储适配
3.整理更详细的文档 配合 com.liulishuo.okdownload 做文件下载 👉 library_file_downloader
4.
参考
管理分区外部存储访问 管理分区外部存储访问 - 如何从原生代码访问媒体文件 & MediaStore增删该查API
- Other
- 参考项目
library_file_downloader
项目基于 OkDownload 实现
-
断点异常的BUG lingochamp/okdownload#39
-
中文文档 https://github.com/lingochamp/okdownload/blob/master/README-zh.md
-
Simple https://github.com/lingochamp/okdownload/wiki/Simple-Use-Guideline
-
Advanced https://github.com/lingochamp/okdownload/wiki/Advanced-Use-Guideline
-
AndroidFilePicker https://github.com/rosuH/AndroidFilePicker/blob/master/README_CN.md
-
FilePicker https://github.com/chsmy/FilePicker
bintrayUpload
novoda
gradlew clean build bintrayUpload -PbintrayUser=javakam -PbintrayKey=xxx -PdryRun=false