- 🚀FileOperator GitHub
- 🚀更简单的处理Android系统文件操作
- 🚀适用于 Android 4.4 及以上系统 , 兼容AndroidQ新的存储策略
- 🚀图片压缩算法参考 Luban
- 🚀Kotlin 案例 👉 app & Java 案例 👉 sample_java
Project build.gradle :
repositories {
maven { url 'https://dl.bintray.com/javakam/maven' }
}
推荐方式 :
implementation 'ando.file:core:1.0.1' //核心库必选
implementation 'ando.file:android-q:1.0.1' //AndroidQ & Android 11 兼容库
implementation 'ando.file:compressor:1.0.1' //图片压缩,核心算法采用 Luban
implementation 'ando.file:selector:1.0.1' //文件选择器
整体引入(不推荐):
implementation 'ando.file:FileOperator:0.9.1'
Application中初始化:
FileOperator.init(this,BuildConfig.DEBUG)
| 功能列表 | 缓存目录 |
|---|---|
| App Specific | MediaStore | Storage Access Framework |
|---|---|---|
| 单图 + 压缩 | 多图 + 压缩 | 多文件 |
|---|---|---|
/* 说明: FileOptions T 为 String.filePath / Uri / File 3M 3145728 Byte ; 5M 5242880 Byte; 10M 10485760 ; 20M = 20971520 Byte */ val optionsImage = FileSelectOptions().apply { fileType = FileType.IMAGE singleFileMaxSize = 2097152 singleFileMaxSizeTip = "图片最大不超过2M!" allFilesMaxSize = 5242880 allFilesMaxSizeTip = "总图片大小不超过5M!" fileCondition = 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 Byte .setAllFilesMaxSize(10485760, "总大小不能超过10M!")// .setMimeTypes(MIME_MEDIA)//默认全部文件, 不同类型系统提供的选择UI不一样 eg: arrayOf("video/*","audio/*","image/*") .applyOptions(optionsImage) //优先使用 FileOptions 中设置的 FileSelectCondition .filter(object : FileSelectCondition { override fun accept(fileType: FileType, uri: Uri?): Boolean { return when (fileType) { FileType.IMAGE -> (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 toastShort("正在压缩图片...") showSelectResult(results) } override fun onError(e: Throwable?) { FileLogger.e("回调 onError ${e?.message}") mTvResultError.text = mTvResultError.text.toString().plus(" 错误信息: ${e?.message} \n") } }) .choose()
val optionsImage = FileSelectOptions().apply { fileType = FileType.IMAGE //maxCount = 2 singleFileMaxSize = 3145728 singleFileMaxSizeTip = "单张图片最大不超过3M!" allFilesMaxSize = 5242880 allFilesMaxSizeTip = "图片总大小不超过5M!" fileCondition = 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 , 单位 Byte .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)//默认全部文件, 不同类型系统提供的选择UI不一样 eg: arrayOf("video/*","audio/*","image/*") .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 toastShort("正在压缩图片...") showSelectResult(results) } override fun onError(e: Throwable?) { FileLogger.e("回调 onError ${e?.message}") mTvResultError.text = mTvResultError.text.toString().plus(" 错误信息: ${e?.message} \n") } }) .choose()
🌴适用于处理复杂文件选择情形, 如: 选取图片、视频文件,其中图片至少选择一张, 最多选择两张, 每张图片大小不超过3M, 全部图片大小不超过5M ; 视频文件只能选择一个, 每个视频大小不超过20M, 全部视频大小不超过30M 。
/* 明: FileOptions T 为 String.filePath / Uri / File 3M 3145728 Byte ; 5M 5242880 Byte; 10M 10485760 ; 20M = 20971520 Byte 50M 52428800 Byte ; 80M 83886080 ; 100M = 104857600 Byte */ //图片 val optionsImage = FileSelectOptions().apply { fileType = FileType.IMAGE maxCount = 2 minCountTip = "至少选择一张图片" maxCountTip = "最多选择两张图片" singleFileMaxSize = 3145728 singleFileMaxSizeTip = "单张图片最大不超过3M!" allFilesMaxSize = 5242880 allFilesMaxSizeTip = "图片总大小不超过5M!" fileCondition = 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 maxCount = 1 minCountTip = "至少选择一个视频文件" maxCountTip = "最多选择一个视频文件" singleFileMaxSize = 20971520 singleFileMaxSizeTip = "单视频最大不超过20M!" allFilesMaxSize = 31457280 allFilesMaxSizeTip = "视频总大小不超过30M!" fileCondition = 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 中设置的单文件大小限制,如果没有设置则采用该值 .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 媒体文件,不同类型系统提供的选择UI不一样 eg: arrayOf("video/*","audio/*","image/*") .applyOptions(optionsImage, optionsVideo) // 优先使用 FileOptions 中设置的 FileSelectCondition,没有的情况下才使用通用的 .filter(object : FileSelectCondition { override fun accept(fileType: FileType, uri: Uri?): Boolean { return when (fileType) { FileType.IMAGE -> (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()
4.压缩图片 ImageCompressor.kt
/** * 压缩图片 1.Luban算法; 2.直接压缩 -> Engine.compress(uri, 100L) * * T 为 String.filePath / Uri / File */ private fun <T> compressImage(photos: List<T>) { ImageCompressor .with(this) .load(photos) .ignoreBy(100)//Byte .setTargetDir(getPathImageCache()) .setFocusAlpha(false) .enableCache(true) .filter(object : ImageCompressPredicate { override fun apply(uri: Uri?): Boolean { FileLogger.i("image predicate $uri ${getFilePathByUri(uri)}") return if (uri != null) { val path = getFilePathByUri(uri) !(TextUtils.isEmpty(path) || (path?.toLowerCase(Locale.getDefault())?.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() }
☘
FileOperator提供了Android开发常用的一些文件操作工具类,使用方式大多以静态方法为主,需要的同学可以直接CV需要的文件
1. 获取文件MimeType类型👉FileMimeType.kt
fun getMimeType(str: String?): String {...}
fun getMimeType(uri: Uri?): String {...}
//MimeTypeMap.getSingleton().getMimeTypeFromExtension(...) 的补充
fun getMimeTypeSupplement(fileName: String): String {...}
2. 计算文件或文件夹的大小👉FileSizeUtils.kt
@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 {}...}
//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}"
}
3. 直接打开Url/Uri(远程or本地)👉FileOpener.kt
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
4. 获取文件Uri/Path👉FileUri.kt
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)
}
}
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)) return uri.path
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { //4.4以后
getPath(context, uri)
} else { //4.4以下
getPathKitkat(context, uri)
}
}
5. 通用文件工具类👉FileUtils.kt
- 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
- 参考项目
项目基于 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
gradlew clean build bintrayUpload -PbintrayUser=javakam -PbintrayKey=xxx -PdryRun=false