我过去使用 MediaStore.Images.Media.insertImage
来保存图片,但是现在这个方法已经被弃用了。根据文档所述:
此方法已于API级别29中弃用。应使用 MediaColumns#IS_PENDING 来插入图像,这可以提供更丰富的生命周期控制。
我不太明白,因为 MediaColumns.IS_PENDING
只是一个标志,我该如何使用它?
我应该使用 ContentValues
吗?
我过去使用 MediaStore.Images.Media.insertImage
来保存图片,但是现在这个方法已经被弃用了。根据文档所述:
此方法已于API级别29中弃用。应使用 MediaColumns#IS_PENDING 来插入图像,这可以提供更丰富的生命周期控制。
我不太明白,因为 MediaColumns.IS_PENDING
只是一个标志,我该如何使用它?
我应该使用 ContentValues
吗?
此方法已在API级别29中弃用。应使用MediaColumns#IS_PENDING执行图像的插入,该方法提供了对生命周期的更丰富控制。
不知道是我太蠢了还是Google团队真的需要重构文档。
无论如何,从链接中提供的答案由CommonsWare和coroutineDispatcher提供。
步骤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
变量,请更新代码好吗? - aldajo92resolver
is the contentResolver
object you get from your applicationContext
- iCantC已解决
@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)
}
}
MediaStore.MediaColumns.IS_PENDING
设置为1会发生什么? - Sam Chen如果您的应用程序已经使用文件提供程序,则可以跳过对AndroidManifest.xml
和filepaths.xml
的添加。
由于在较早版本的Android中似乎会崩溃,因此我不得不将getExternalStoragePublicDirectory(DIRECTORY_PICTURES)
替换为applicationContext.getExternalFilesDir(DIRECTORY_PICTURES)
。请确保您在filepaths.xml
中提供根目录。
/**
* 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) }
}
}
/**
* 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) }
}
}
<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>
<?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
感谢iCantC对步骤2:将图像保存为Q样式做出的贡献。
我在使用Android Studio时遇到了一些内存使用问题,不得不打开Sublime进行修复。为了解决这个错误:
e: java.lang.OutOfMemoryError: Java heap space
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!!
}
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>
我正在添加第二个答案,因为我不确定是否有人在意版本检查,但如果你在意,还有更多步骤,嗯...... 从
步骤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>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="atMostJellyBeanMR2">true</bool>
<bool name="atMostKitkat">false</bool>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="atMostJellyBeanMR2">false</bool>
<bool name="atMostKitkat">true</bool>
</resources>
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)
}
}
}
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()
}
}
ContentValues
调用insert()
方法获取一个Uri
,可以用于写出你的内容。对于IS_PENDING
部分,在insert()
方法中将IS_PENDING
设置为1
。然后,在写出内容后,将该项的IS_PENDING
设置为0
,并使用update()
方法进行更新。参见此代码片段进行示例,但在我的情况下是保存视频而不是图像。 - CommonsWareuri?.let { resolver.openOutputStream(uri)?.use { outputStream -> val sink = Okio.buffer(Okio.sink(outputStream)) response.body()?.source()?.let { sink.writeAll(it) } sink.close() }
- coroutineDispatcherresponse.body().source()
给了我一个OkioSource
,代表我正在下载的视频字节。Okio.buffer(Okio.sink(outputStream))
给了我一个OkioSink
,表示我要写入字节的位置,writeAll()
将所有来自Source
的字节写入Sink
。请参见此SO答案以获取Square批准的方法。 - CommonsWare