如何将图像保存在 android Q 的子目录中,同时保持向后兼容

Posted

技术标签:

【中文标题】如何将图像保存在 android Q 的子目录中,同时保持向后兼容【英文标题】:How to save an image in a subdirectory on android Q whilst remaining backwards compatible 【发布时间】:2019-11-23 15:20:57 【问题描述】:

我正在创建一个简单的图像编辑器应用程序,因此需要加载和保存图像文件。我希望保存的文件出现在画廊中的单独相册中。从 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”文件夹中。

【问题讨论】:

我的猜测是您将需要使用两种不同的存储策略,一种用于 API Level 29+,另一种用于旧设备。 “在这种情况下可以使用不推荐使用的代码吗” - 是的。像这样的东西“弃用”意味着“我们希望你使用其他东西”。如果您在较新的 Android 版本上使用这些“其他东西”,则在较旧的 Android 版本上运行的代码可以毫无问题地使用已弃用的 API。很少会从 SDK 中删除类和方法,这样您的代码将不再构建,我不希望您的任何选项都会发生这种情况。 @CommonsWare 感谢您的想法,这正是我现在正在做的事情,我想这是唯一可行的方法。如果您愿意,可以将您的评论转换为答案,我会接受。 @multimodcrafter 你是如何让图像保存工作的? insert 方法的 uri 基本上类似于 content://media/external/images/media/123 我无法从中获取文件路径。你介意发布代码吗?谢谢, 其实我只是想通了。感谢@multimodcrafter,如果没有你的帖子,我将无法在 Android Q 中将图片保存到图库中。 @DavidSantiagoTuriño 不错。我使用contentResolver.openOutputStream(uri) 直接基于uri 创建了一个流,我猜在引擎盖下它与您的方法相同。我的代码记录在这里:***.com/questions/36624756/… 【参考方案1】:

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

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

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

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

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

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

【讨论】:

经过实际测试(三星galaxy s10),DATA栏目在Android Q上仍然可用。我想你误解了文档的意思。它说您无法使用文件路径访问文件,而不是无法获取路径本身。 @Chenhe:“DATA 列在 Android Q 上仍然可用”——在我写这篇文章和 Android 10 发布之间的某个时间,规则也可能发生了变化,或者我的回答是基于之前的Android Q 的开发者预览版。 @CommonsWare 但是您刚才提到的示例是使用两种不同的方法来保存文件/downloadLegacy() 内部的旧方法和download() 中的android Q 及更高版本的新方法 @superjos:在 Android 9 及更早版本上,您应该可以使用 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) 获取 Videos 目录,然后在该目录下创建一个子目录。我的示例正好直接下载到 Videos,因为我写示例的时候很懒。 感谢您的帮助。对于其他可能像我一样忽略的人:Environment.DIRECTORY_MOVIES 可能还不存在,所以在忙于创建自定义子目录 (mySubDir.mkdir()) 时,应确保整个子树存在 (mySubDir.mkdirs())【参考方案2】:

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


【讨论】:

【参考方案3】:

对于旧 API (

让我们看看我的代码。

此函数创建一个图像文件。注意 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)
        
    

【讨论】:

【参考方案4】:

经过反复试验,我发现可以以向后兼容的方式使用MediaStore,以便在不同版本的实现之间共享尽可能多的代码。唯一的窍门是记住,如果您使用MediaColumns.DATA,您需要自己创建文件

让我们看看code from my project (Kotlin)。此示例用于保存音频,而不是图像,但您只需将 MIME_TYPEDIRECTORY_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 中提取文件描述符并将其返回以供使用。

【讨论】:

以上是关于如何将图像保存在 android Q 的子目录中,同时保持向后兼容的主要内容,如果未能解决你的问题,请参考以下文章

如何将图像保存在android画廊中

如何将包含内容的私有文件夹复制到 android q 及更高版本中的公共目录?

将图像从 Android 上的可绘制资源保存到 sdcard

如何使用 as3 将 jpg 图像保存到 Android 手机

如何在android中保存从相机拍摄的图像

android如何将捕获的图像保存到手机库中