https://juejin.cn/post/6844904063432130568
https://developer.android.com/training/data-storage?hl=zh-cn
https://developer.android.com/about/versions/11/privacy/storage?hl=zh-cn#test-scoped-storage
https://developer.android.com/guide/topics/providers/content-providers
Android 9 以下
Android10 可以使用requestLegacyExternalStorage=true,退出分区存储。
Android11 运行 targetSdkVersion = 29 仍可以请求requestLegacyExternalStorage,targetSdkVersion =30,系统会忽略requestLegacyExternalStorage标记
分区存储是否开启
是否需要读写权限
创建是否成功
error
开启
需要
失败
FileOutputStream throws FileNotFoundException
关闭
需要
成功
保存图片方式
是否需要读写权限
是否支持分区存储
开启分区存储
FileOutputStream
需要写权限
不支持
FileOutputStream throws FileNotFoundException
ContentProvider.insert
开启分区存储-不需要 未开启分区存储-需要写权限
支持
success
suspend fun saveImage (bitmap : Bitmap ): String? = withContext(Dispatchers .IO ) {
val path = Environment .getExternalStoragePublicDirectory(Environment .DIRECTORY_DCIM ).absolutePath + File .separator + " Camera"
val file = File (path)
val fileOutputStream: FileOutputStream
// 文件夹不存在,则创建它
if (! file.exists()) {
file.mkdir()
}
try {
val imgName = path + File .separator + System .currentTimeMillis() + " .jpg"
fileOutputStream = FileOutputStream (imgName)
val isSuccess = bitmap.compress(Bitmap .CompressFormat .JPEG , 100 , fileOutputStream)
fileOutputStream.flush()
fileOutputStream.close()
return @withContext if (isSuccess) imgName else null
} catch (e: Exception ) {
e.printStackTrace()
}
return @withContext null
}
2. saveBitmap - ContentProvider
suspend fun saveBitmapToPictures (context : Context , bitmap : Bitmap ): Boolean = withContext(Dispatchers .IO ) {
val fileName = " ${System .currentTimeMillis()} .jpg"
val contentValues = ContentValues ().apply {
put(MediaStore .MediaColumns .DISPLAY_NAME , fileName)
put(MediaStore .MediaColumns .MIME_TYPE , " image/jpeg" )
}
val path = getAppPicturePath()
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
contentValues.put(MediaStore .MediaColumns .RELATIVE_PATH , path)
} else {
val fileDir = File (path)
if (! fileDir.exists()) {
fileDir.mkdir()
}
contentValues.put(MediaStore .MediaColumns .DATA , path + File .separator + fileName)
}
context.contentResolver.insert(MediaStore .Images .Media .EXTERNAL_CONTENT_URI , contentValues)?.let {
try {
context.contentResolver.openOutputStream(it).use {
bitmap.compress(Bitmap .CompressFormat .JPEG , 100 , it)
return @withContext true
}
} catch (exception: FileNotFoundException ) {
exception.printStackTrace()
return @withContext false
}
}
false
}
private fun getAppPicturePath (): String {
return if (Build .VERSION .SDK_INT < Build .VERSION_CODES .Q ) {
Environment .getExternalStorageDirectory().absolutePath + File .separator + " Camera"
} else {
Environment .DIRECTORY_DCIM + File .separator + " Camera"
}
}
调起相机适配,指定图片保存图片的位置 - 通过ContentProvider获取需要插入的Uri
/* *
* 拍照后保存位置
*/
captureIntent.putExtra(MediaStore .EXTRA_OUTPUT , insertPhoto());
/* *
* 相册中插入图片
*/
private Uri insertPhoto() {
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
currentPhotoUri = createImageUri();
} else {
// 第一种方式
File photoFile = createImageFile();
if (photoFile != null ) {
currentPhotoUri = FileProvider .getUriForFile(mContext.get(), captureStrategy.authority, photoFile);
}
// 第二种
currentPhotoUri = createImageUri();
}
return currentPhotoUri;
}
/* *
* 创建图片地址uri,用于保存拍照后的照片
*
* @return 图片的uri
*/
private Uri createImageUri() {
String status = Environment .getExternalStorageState();
if (status.equals(Environment .MEDIA_MOUNTED )) {
return mContext.get().getContentResolver().insert(MediaStore .Images .Media .EXTERNAL_CONTENT_URI , new ContentValues ());
} else {
return mContext.get().getContentResolver().insert(MediaStore .Images .Media .INTERNAL_CONTENT_URI , new ContentValues ());
}
}
/* *
* 创建图片地址,用于保存图片
*/
private File createImageFile() {
// Create an image file name
String timeStamp = FormatterUtilsKt .formatDate(System .currentTimeMillis(), " yyyyMMdd_HHmmss" );
String imageFileName = String .format(" JPEG_%s.jpg" , timeStamp);
File storageDir;
if (captureStrategy.isPublic) {
storageDir = Environment .getExternalStoragePublicDirectory(Environment .DIRECTORY_PICTURES );
if (! storageDir.exists()) storageDir.mkdirs();
} else {
storageDir = mContext.get().getExternalFilesDir(Environment .DIRECTORY_PICTURES );
}
if (captureStrategy.directory != null ) {
storageDir = new File (storageDir, captureStrategy.directory);
if (! storageDir.exists()) storageDir.mkdirs();
}
// Avoid joining path components manually
File tempFile = new File (storageDir, imageFileName);
// Handle the situation that user's external storage is not ready
if (! Environment .MEDIA_MOUNTED .equals(EnvironmentCompat .getStorageState(tempFile))) {
return null ;
}
return tempFile;
}
系统裁剪图片适配,指定图片保存的位置- 通过ContentProvider获取需要插入的Uri
/* *
* 裁剪后保存位置
*/
intent.putExtra(MediaStore .EXTRA_OUTPUT , insertClipPhoto())
/* *
* 相册中插入裁切图片
*/
private Uri insertClipPhoto() {
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
currentClipUri = createImageUri();
} else {
// 第一种
currentClipUri = Uri .fromFile(createImageFile());
// 第二种
currentClipUri = createImageUri();
}
return currentClipUri;
}
onActivityResult适配 -统一使用Uri来操作图片
Action
权限说明 - 开启分区存储
权限说明 - 未开启分区存储
调用系统相册后展示图片-contentResolver.openInputStream(Uri)
不需要
不需要
指定相册插入位置-通过创建File获取插入Uri
不支持-throws FileNotFoundException
需要写权限
指定相册插入位置-ContentProvider.insert
不需要读写权限
需要写权限
内部通过File 获取 大小、尺寸 等信息 来进行压缩 →需要有权限操作File
压缩后生成的文件名称为时间戳
无法通过使用copy文件与压缩后文件一致,覆写来减少临时文件的管理,只能通过压缩后删除临时文件
多次选择同一个图片压缩后会生成多张图片
不满足压缩条件会返回原文件
将共享目录拷贝到cache,如果发生压缩,完成后删除cache,如果未发生压缩,则保留cache.
将共享目录拷贝到cache目录(与target一致):data/data/packagename/files/demo/imag
fun copyToCache (context : Context , uri : Uri ): File {
val file = File (" ${cachePath(context)}${uri.fileName(context)} " )
file.parentFile?.mkdirs()
context.contentResolver.openInputStream(uri).use { input ->
file.outputStream().use { output ->
input?.copyTo(output, DEFAULT_BUFFER_SIZE )
}
}
return file
}
override fun onSuccess (file : File ) {
if (file != copyFile) copyFile.delete()
onSuccess(file, index)
}
整理的luBanFile(统一固定配置,减少参数传递,删除多余的方法)
fun luBanUri (context : Context , sourceFile : Uri , size : Int = 200, targetDir : String = cachePath(context), onSuccess : (File , Int ) -> Unit , onStart : () -> Unit = {}, onError : (e: Throwable ) -> Unit = {}, index : Int = -1) {
val copyFile = copyToCache(context, sourceFile)
createDirNoExist(targetDir)
Luban .with (context)
.load(copyFile)
.ignoreBy(size)
.setTargetDir(targetDir)
.setCompressListener(object : OnCompressListener {
override fun onStart () {
onStart()
}
override fun onSuccess (file : File ) {
if (file != copyFile) copyFile.delete()
onSuccess(file, index)
}
override fun onError (e : Throwable ) {
onError(e)
}
}).launch()
}
从FileProvier 限制 使用 FileUri的出,到限制FileUri的入
遵循分区存储三个原则对外部存储文件访问方式重新设计。
https://github.com/many-cat/ScopedStorageTest