我正在创建一个简单的图像编辑器应用程序,因此需要加载和保存图像文件。我希望保存的文件出现在图库中的单独相册中。从 Android API 28 到 29,应用程序访问存储的程度发生了巨大变化。我可以在 Android Q (API 29) 中做我想做的事情,但这种方式不向后兼容。
当我想在较低的 API 版本中实现相同的结果时,到目前为止我只找到了方法,这需要使用已弃用的代码(从 API 29 开始)。
其中包括:
MediaStore.Images.Media.DATA
栏的使用Environment.getExternalStoragePublicDirectory(...)
MediaStore.Images.Media.insertImage(...)
我的问题是:是否可以以这种方式实现它,使其向后兼容,但不需要已弃用的代码?如果不是,在这种情况下是否可以使用已弃用的代码,或者这些方法很快就会从 sdk 中删除吗?无论如何,使用已弃用的方法感觉非常糟糕,所以我宁愿不:)
这是我发现适用于 API 29 的方式:
ContentValues values = new ContentValues();
String filename = System.currentTimeMillis() + ".jpg";
values.put(MediaStore.Images.Media.TITLE, filename);
values.put(MediaStore.Images.Media.DISPLAY_NAME, filename);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
values.put(MediaStore.Images.Media.RELATIVE_PATH, "PATH/TO/ALBUM");
getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values);
然后我使用 insert 方法返回的 URI 来保存位图。问题是 API 29 中引入了字段 RELATIVE_PATH,因此当我在较低版本上运行代码时,图像被放入“Pictures”文件夹中,而不是“PATH/TO/ALBUM”文件夹中。
在这种情况下可以使用已弃用的代码吗?或者这些方法很快就会从 sdk 中删除吗?
DATA
选项在Android Q上不起作用,因为该数据不包含在query()
结果中,即使您要求它,您也不能使用它返回的路径,即使它们返回了。
默认情况下,
Environment.getExternalStoragePublicDirectory(...)
选项在 Android Q 上不起作用,但您可以添加清单条目来重新启用它。但是,该清单条目可能会在 Android R 中删除,因此除非您时间紧迫,否则我不会走这条路。
据我所知,
MediaStore.Images.Media.insertImage(...)
仍然有效,尽管它已被弃用。
是否可以以这种方式实现它,以便向后兼容,但不需要已弃用的代码?
我的猜测是,您将需要使用两种不同的存储策略,一种用于 API 级别 29+,另一种用于旧设备。我在这个示例应用程序中采用了这种方法,尽管我在那里处理视频内容,而不是图像,所以
insertImage()
不是一个选项。
这是适合我的代码。此代码将图像保存到手机上的子目录文件夹中。它检查手机的 android 版本,如果高于 android q,则运行所需的代码,如果低于,则运行 else 语句中的代码。
来源:https://androidnoon.com/save-file-in-android-10-and-below-using-scoped-storage-in-android-studio/
private void saveImageToStorage(Bitmap bitmap) throws IOException {
OutputStream imageOutStream;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME,
"image_screenshot.jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + File.separator + "AppName");
Uri uri =
getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values);
imageOutStream = getContentResolver().openOutputStream(uri);
} else {
String imagesDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES). toString() + "/AppName";
File image = new File(imagesDir, "image_screenshot.jpg");
imageOutStream = new FileOutputStream(image);
}
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream);
imageOutStream.close();
}
对于旧 API (<29) I place an image into the external media directory and scan it via MediaScannerConnection.
让我们看看我的代码。
此函数创建一个图像文件。注意 appName 变量 - 它是显示图像的相册的名称。
override fun createImageFile(appName: String): File {
val dir = File(appContext.externalMediaDirs[0], appName)
if(!dir.exists()) {
ir.mkdir()
}
return File(dir, createFileName())
}
然后,我将图像放入文件中,最后,我运行媒体扫描仪,如下所示:
private suspend fun scanNewFile(shot: File): Uri? {
return suspendCancellableCoroutine { continuation ->
MediaScannerConnection.scanFile(
appContext,
arrayOf<String>(shot.absolutePath),
arrayOf(imageMimeType)) { _, uri -> continuation.resume(uri)
}
}
}
经过一些试验和错误,我发现可以以向后兼容的方式使用
MediaStore
,这样不同版本的实现之间可以共享尽可能多的代码。唯一的技巧是记住,如果您使用 MediaColumns.DATA
,您需要自己创建文件。
让我们看一下我的项目中的代码(Kotlin)。此示例用于保存音频,而不是图像,但您只需将
MIME_TYPE
和 DIRECTORY_MUSIC
替换为您需要的任何内容。
private fun newFile(): FileDescriptor? {
// Create a file descriptor for a new recording.
val date = DateFormat.getDateTimeInstance().format(Calendar.getInstance().time)
val filename = "$date.mp3"
val values = ContentValues().apply {
put(MediaColumns.TITLE, date)
put(MediaColumns.MIME_TYPE, "audio/mp3")
// store the file in a subdirectory
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaColumns.DISPLAY_NAME, filename)
put(MediaColumns.RELATIVE_PATH, saveTo)
} else {
// RELATIVE_PATH was added in Q, so work around it by using DATA and creating the file manually
@Suppress("DEPRECATION")
val music = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).path
with(File("$music/P2oggle/$filename")) {
@Suppress("DEPRECATION")
put(MediaColumns.DATA, path)
parentFile!!.mkdir()
createNewFile()
}
}
}
val uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values)!!
return contentResolver.openFileDescriptor(uri, "w")?.fileDescriptor
}
在 Android 10 及更高版本上,我们使用
DISPLAY_NAME
设置文件名,使用 RELATIVE_PATH
设置子目录。在旧版本上,我们使用 DATA
并手动创建文件(及其目录)。此后,两者的实现是相同的:我们只需从 MediaStore
中提取文件描述符并将其返回以供使用。