MediaStore.Images.Media.insertImage已被弃用

45

我过去使用 MediaStore.Images.Media.insertImage 来保存图片,但是现在这个方法已经被弃用了。根据文档所述:

此方法已于API级别29中弃用。应使用 MediaColumns#IS_PENDING 来插入图像,这可以提供更丰富的生命周期控制。

我不太明白,因为 MediaColumns.IS_PENDING 只是一个标志,我该如何使用它?

我应该使用 ContentValues 吗?


3
使用一个 ContentValues 调用 insert() 方法获取一个 Uri,可以用于写出你的内容。对于 IS_PENDING 部分,在 insert() 方法中将 IS_PENDING 设置为 1。然后,在写出内容后,将该项的 IS_PENDING 设置为 0,并使用 update() 方法进行更新。参见此代码片段进行示例,但在我的情况下是保存视频而不是图像。 - CommonsWare
问题:这段代码发生了什么?uri?.let { resolver.openOutputStream(uri)?.use { outputStream -> val sink = Okio.buffer(Okio.sink(outputStream)) response.body()?.source()?.let { sink.writeAll(it) } sink.close() } - coroutineDispatcher
在我的情况下,我正在从URL下载视频到设备。response.body().source() 给了我一个Okio Source,代表我正在下载的视频字节。 Okio.buffer(Okio.sink(outputStream))给了我一个Okio Sink,表示我要写入字节的位置,writeAll()将所有来自Source的字节写入Sink。请参见此SO答案以获取Square批准的方法。 - CommonsWare
请查看我在此处的答案:https://dev59.com/pF0a5IYBdhLWcg3wipTk#68110559 - Amr
6个回答

19

此方法已在API级别29中弃用。应使用MediaColumns#IS_PENDING执行图像的插入,该方法提供了对生命周期的更丰富控制。

不知道是我太蠢了还是Google团队真的需要重构文档。

无论如何,从链接中提供的答案由CommonsWarecoroutineDispatcher提供。

步骤1:决定您所使用的API级别

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) saveImageInQ(imageBitMap)
    else saveImageInLegacy(imageBitMap)

步骤2:将图像保存为 Q 样式

//Make sure to call this function on a worker thread, else it will block main thread
fun saveImageInQ(bitmap: Bitmap):Uri {   
    val filename = "IMG_${System.currentTimeMillis()}.jpg"
    var fos: OutputStream? = null
    val imageUri: Uri? = null
    val contentValues = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
        put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
        put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
        put(MediaStore.Video.Media.IS_PENDING, 1)
    }

    //use application context to get contentResolver
    val contentResolver = application.contentResolver

    contentResolver.also { resolver ->               
        imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
        fos = imageUri?.let { resolver.openOutputStream(it) }
    }

    fos?.use { bitmap.compress(Bitmap.CompressFormat.JPEG, 70, it) }

    contentValues.clear()
    contentValues.put(MediaStore.Video.Media.IS_PENDING, 0)
    resolver.update(imageUri, contentValues, null, null)
          
    return imageUri
}

第三步: 如果不在 Q 上,则以传统样式保存图像。

//Make sure to call this function on a worker thread, else it will block main thread
fun saveTheImageLegacyStyle(bitmap:Bitmap){
    val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
    val image = File(imagesDir, filename)
    fos = FileOutputStream(image)
    fos?.use {bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)}
}

这应该能让您开始了!


在第二步中,缺少resolver变量,请更新代码好吗? - aldajo92
resolver is the contentResolver object you get from your applicationContext - iCantC
感谢您完成的代码...我使用了您的函数saveImageInQ,它的效果非常好。 - reza rahmad

18

已解决

@CommonsWare建议的代码没有问题,但是如果您使用targetSdkVersion 29进行编程,必须添加以下条件:

val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis().toString())
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one
                put(MediaStore.MediaColumns.RELATIVE_PATH, relativeLocation)
                put(MediaStore.MediaColumns.IS_PENDING, 1)
            }
        }

之后你如何使用这些内容值? - Jesus Almaral - Hackaprende
看看这个类,我使用了完全相同的代码,因为它是这种用例: https://github.com/coroutineDispatcher/pocket_treasure/blob/master/gallery_module/src/main/java/com/sxhardha/gallery_module/image/FullImageFragment.kt - coroutineDispatcher
如果我没有将MediaStore.MediaColumns.IS_PENDING设置为1会发生什么? - Sam Chen
你所写的文件将无法被其他应用程序访问。 - Yogesh Seralia

6

所有答案的总结,以及每个答案的重构版本。

如果您的应用程序已经使用文件提供程序,则可以跳过对AndroidManifest.xmlfilepaths.xml的添加。

更新(12/2022)

由于在较早版本的Android中似乎会崩溃,因此我不得不将getExternalStoragePublicDirectory(DIRECTORY_PICTURES)替换为applicationContext.getExternalFilesDir(DIRECTORY_PICTURES)。请确保您在filepaths.xml中提供根目录。

保存为PNG格式
/**
 * Saves a bitmap as a PNG file.
 *
 * Note that `.png` extension is added to the filename.
 */
fun Bitmap.saveAsPNG(filename: String) = "$filename.png".let { name ->
    if (SDK_INT < Q) {
        @Suppress("DEPRECATION")
        val file = File(applicationContext.getExternalFilesDir(DIRECTORY_PICTURES), name)
        FileOutputStream(file).use { compress(PNG, 100, it) }
        MediaScannerConnection.scanFile(applicationContext,
            arrayOf(file.absolutePath), null, null)
        FileProvider.getUriForFile(applicationContext,
            "${ applicationContext.packageName }.provider", file)
    } else {
        val values = ContentValues().apply {
            put(DISPLAY_NAME, name)
            put(MIME_TYPE, "image/png")
            put(RELATIVE_PATH, DIRECTORY_DCIM)
            put(IS_PENDING, 1)
        }

        val resolver = applicationContext.contentResolver
        val uri = resolver.insert(EXTERNAL_CONTENT_URI, values)
        uri?.let { resolver.openOutputStream(it) }
            ?.use { compress(PNG, 100, it) }

        values.clear()
        values.put(IS_PENDING, 0)
        uri?.also {
            resolver.update(it, values, null, null) }
    }
}
另存为JPG
/**
 * Saves a bitmap as a Jpeg file.
 *
 * Note that `.jpg` extension is added to the filename.
 */
fun Bitmap.saveAsJPG(filename: String) = "$filename.jpg".let { name ->
    if (SDK_INT < Q)
        @Suppress("DEPRECATION")
        FileOutputStream(File(applicationContext.getExternalFilesDir(DIRECTORY_PICTURES), name))
            .use { compress(JPEG, 100, it) }
    else {
        val values = ContentValues().apply {
            put(DISPLAY_NAME, name)
            put(MIME_TYPE, "image/jpg")
            put(RELATIVE_PATH, DIRECTORY_PICTURES)
            put(IS_PENDING, 1)
        }

        val resolver = applicationContext.contentResolver
        val uri = resolver.insert(EXTERNAL_CONTENT_URI, values)
        uri?.let { resolver.openOutputStream(it) }
            ?.use { compress(JPEG, 70, it) }

        values.clear()
        values.put(IS_PENDING, 0)
        uri?.also {
            resolver.update(it, values, null, null) }
    }
}
AndroidManifest.xml
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/filepaths" />
</provider>
res/xml/filepaths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <cache-path
        name="whatever"
        path="/" />
</paths>
导入(如果您缺少某些内容)
import android.content.ContentValues
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat.JPEG
import android.graphics.Bitmap.CompressFormat.PNG
import android.media.MediaScannerConnection
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.Q
import android.os.Environment.DIRECTORY_DCIM
import android.os.Environment.DIRECTORY_PICTURES
import android.os.Environment.getExternalStoragePublicDirectory
import android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
import android.provider.MediaStore.MediaColumns.DISPLAY_NAME
import android.provider.MediaStore.MediaColumns.MIME_TYPE
import android.provider.MediaStore.MediaColumns.RELATIVE_PATH
import android.provider.MediaStore.Video.Media.IS_PENDING
import androidx.core.content.FileProvider
import java.io.File
import java.io.FileOutputStream

有 Java 版本吗? - Eyes Blue

3

感谢iCantC步骤2:将图像保存为Q样式做出的贡献。

我在使用Android Studio时遇到了一些内存使用问题,不得不打开Sublime进行修复。为了解决这个错误:

e: java.lang.OutOfMemoryError: Java heap space

这是我作为示例用PNG图像的代码,任何小于100的bitmap.compress值可能没有用。 以前的版本在API 30上无法使用,因此我将contentValues RELATIVE_PATH更新为DIRECTORY_DCIM,并且还更新了contentResolver.insert(EXTERNAL_CONTENT_URI, ...
   
    private val dateFormatter = SimpleDateFormat(
        "yyyy.MM.dd 'at' HH:mm:ss z", Locale.getDefault()
    )
    private val legacyOrQ: (Bitmap) -> Uri = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
        saveImageInQ(it) else legacySave(it) }
    
    private fun saveImageInQ(bitmap: Bitmap): Uri {
        val filename = "${title}_of_${dateFormatter.format(Date())}.png"
        val fos: OutputStream?
        val contentValues = ContentValues().apply {
            put(DISPLAY_NAME, filename)
            put(MIME_TYPE, "image/png")
            put(RELATIVE_PATH, DIRECTORY_DCIM)
            put(IS_PENDING, 1)
        }

        //use application context to get contentResolver
        val contentResolver = applicationContext.contentResolver
        val uri = contentResolver.insert(EXTERNAL_CONTENT_URI, contentValues)
        uri?.let { contentResolver.openOutputStream(it) }.also { fos = it }
        fos?.use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
        fos?.flush()
        fos?.close()

        contentValues.clear()
        contentValues.put(IS_PENDING, 0)
        uri?.let {
            contentResolver.update(it, contentValues, null, null)
        }
        return uri!!
    }

步骤3:如果不在Q平台上,请以传统方式保存图像。
private fun legacySave(bitmap: Bitmap): Uri {
        val appContext = applicationContext
        val filename = "${title}_of_${dateFormatter.format(Date())}.png"
        val directory = getExternalStoragePublicDirectory(DIRECTORY_PICTURES)
        val file = File(directory, filename)
        val outStream = FileOutputStream(file)
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream)
        outStream.flush()
        outStream.close()
        MediaScannerConnection.scanFile(appContext, arrayOf(file.absolutePath),
            null, null)
        return FileProvider.getUriForFile(appContext, "${appContext.packageName}.provider",
            file)
    }

步骤4:创建自定义的FileProvider

package com.example.background.workers.provider

import androidx.core.content.FileProvider

class WorkerFileProvider : FileProvider() {

}

步骤五:更新AndroidManifest.xml文件

从以下内容进行更改:

<activity android:name=".MyActivity" />

to

<activity android:name=".MyActivity">
            <intent-filter>
                <action android:name="android.intent.action.PICK"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.OPENABLE"/>
                <data android:mimeType="image/png"/>
            </intent-filter>
        </activity>
        <provider
            android:name=".workers.provider.WorkerFileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true"
            android:permission="android.permission.MANAGE_DOCUMENTS">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER"/>
            </intent-filter>
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>

步骤6:在xml文件下为FILE_PROVIDER_PATHS添加资源 在我的情况下,我需要添加图片文件夹。

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="pictures" path="Pictures"/>
</paths>

1

我正在添加第二个答案,因为我不确定是否有人在意版本检查,但如果你在意,还有更多步骤,嗯...... 从

步骤5:更新AndroidManifest.xml文件

更改自

<activity android:name=".MyActivity" />

        <activity android:name=".legacy.LegacyMyActivity"/>
        <activity android:name=".MyActivity" />
        <provider
            android:name=".workers.provider.WorkerFileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true"
            android:enabled="@bool/atMostKitkat"
            android:permission="android.permission.MANAGE_DOCUMENTS">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER"/>
            </intent-filter>
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>
        <activity-alias android:name=".legacy.LegacyMyActivity"
            android:targetActivity=".MyActivity"
            android:enabled="@bool/atMostJellyBeanMR2">
            <intent-filter>
                <action android:name="android.intent.action.PICK" />
                <category android:name="android.intent.category.OPENABLE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="image/png" />
            </intent-filter>
        </activity-alias>

步骤6:在XML下添加资源以用于FILE_PROVIDER_PATHS,具体方法如前所述。
步骤7:在res/values下添加资源以用于bool.xml。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="atMostJellyBeanMR2">true</bool>
    <bool name="atMostKitkat">false</bool>
</resources>

第8步:在res/values-v19文件夹下再创建一个。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <bool name="atMostJellyBeanMR2">false</bool>
    <bool name="atMostKitkat">true</bool>
</resources>

步骤9:最后,如果您需要查看保存的文件,则重要更改是actionView.addFlags(FLAG_GRANT_READ_URI_PERMISSION)
   binding.seeFileButton.setOnClickListener {
        viewModel.outputUri?.let { currentUri ->
                 val actionView = Intent(Intent.ACTION_VIEW, currentUri)
                 actionView.addFlags(FLAG_GRANT_READ_URI_PERMISSION)
                 actionView.resolveActivity(packageManager)?.run {
                    startActivity(actionView)
             }
        }
   }

有些人会认为版本检查是一个重要的考虑因素,我同意这一点,但不知道它有多重要。另外,我想让我的原始答案简短一些。 - Manav Brar

0
fun saveImage29(bitmap: Bitmap){
        val insertUri =
            contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, ContentValues())
        try {
            val outputStream = insertUri?.let { contentResolver.openOutputStream(it, "rw") }
            if (bitmap.compress(Bitmap.CompressFormat.JPEG,90,outputStream)){
                toast("保存成功")
            }else{
                toast("保存失败")
            }
        }catch (e:FileNotFoundException){
            e.printStackTrace()
        }

    }

您的回答可以通过添加更多支持性信息来改进。请[编辑]以添加进一步的细节,例如引用或文档,以便其他人可以确认您的答案是否正确。您可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接