21

我正在创建一个简单的图像编辑器应用程序,因此需要加载和保存图像文件。我希望保存的文件出现在画廊中的单独相册中。从 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 字段,因此当我在较低版本上运行代码时,图像被放入“图片”文件夹而不是“PATH/TO/ALBUM”文件夹。

4

4 回答 4

7

在这种情况下可以使用已弃用的代码,还是这些方法很快会从 sdk 中删除?

DATA选项在 Android Q 上不起作用,因为该数据不包含在query()结果中,即使您要求它,您也不能使用它返回的路径,即使它们被返回。

默认情况下,该Environment.getExternalStoragePublicDirectory(...)选项在 Android Q 上不起作用,但您可以添加清单条目以重新启用它。但是,该清单条目可能会在 Android R 中删除,因此除非您时间紧迫,否则我不会走这条路。

AFAIK,MediaStore.Images.Media.insertImage(...)仍然有效,即使它已被弃用。

是否有可能以这种方式实现它,所以它是向后兼容的,但不需要弃用的代码?

我的猜测是您将需要使用两种不同的存储策略,一种用于 API Level 29+,另一种用于旧设备。我在这个示例应用程序中采用了这种方法,尽管我正在处理视频内容,而不是图像,所以insertImage()不是一个选项。

于 2019-07-22T11:36:13.740 回答
3

这是对我有用的代码。此代码将图像保存到手机上的子目录文件夹中。它检查手机的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.pathSeparator + "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();
    

}
于 2020-06-26T19:05:16.247 回答
1

对于旧 API (<29),我将图像放入外部媒体目录并通过 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)
        }
    }
}
于 2020-09-22T19:31:02.473 回答
0

经过反复试验,我发现可以以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并将其返回以供使用。

于 2021-06-13T12:40:10.313 回答