如何使用 MediaStore 在 AndroidQ 中保存图像?
How to save an image in Android Q using MediaStore?
这里有个link给新的Android问Scoped Storage.
根据this Android Developers Best Practices Blog, storing shared media files
(which is my case) should be done using the MediaStore API.
深入研究文档,我找不到相关函数。
这是我在 Kotlin 中的试用:
val bitmap = getImageBitmap() // I have a bitmap from a function or callback or whatever
val name = "example.png" // I have a name
val picturesDirectory = getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
// Make sure the directory "Android/data/com.mypackage.etc/files/Pictures" exists
if (!picturesDirectory.exists()) {
picturesDirectory.mkdirs()
}
try {
val out = FileOutputStream(File(picturesDirectory, name))
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
out.flush()
out.close()
} catch(e: Exception) {
// handle the error
}
结果是我的图片保存在这里 Android/data/com.mypackage.etc/files/Pictures/example.png
正如最佳实践博客中描述的 Storing app-internal files
我的问题是:
如何使用 MediaStore 保存图片API?
Java 中的答案同样可以接受。
提前致谢!
编辑
感谢 PerracoLabs
这真的很有帮助!
不过还有3分
这是我的代码:
val name = "Myimage"
val relativeLocation = Environment.DIRECTORY_PICTURES + File.pathSeparator + "AppName"
val contentValues = ContentValues().apply {
put(MediaStore.Images.ImageColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
// without this part causes "Failed to create new MediaStore record" exception to be invoked (uri is null below)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.ImageColumns.RELATIVE_PATH, relativeLocation)
}
}
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
var stream: OutputStream? = null
var uri: Uri? = null
try {
uri = contentResolver.insert(contentUri, contentValues)
if (uri == null)
{
throw IOException("Failed to create new MediaStore record.")
}
stream = contentResolver.openOutputStream(uri)
if (stream == null)
{
throw IOException("Failed to get output stream.")
}
if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream))
{
throw IOException("Failed to save bitmap.")
}
Snackbar.make(mCoordinator, R.string.image_saved_success, Snackbar.LENGTH_INDEFINITE).setAction("Open") {
val intent = Intent()
intent.type = "image/*"
intent.action = Intent.ACTION_VIEW
intent.data = contentUri
startActivity(Intent.createChooser(intent, "Select Gallery App"))
}.show()
} catch(e: IOException) {
if (uri != null)
{
contentResolver.delete(uri, null, null)
}
throw IOException(e)
}
finally {
stream?.close()
}
1- 保存的图像没有得到正确的名称"Myimage.png"
我尝试使用 "Myimage" 和 "Myimage.PNG" 但都没有用。
图片的名称总是由数字组成,例如:
1563468625314.jpg
这给我们带来了第二个问题:
2- 即使我以 png
.
格式压缩位图,图像仍保存为 jpg
问题不大。只是好奇为什么。
3- relativeLocation 位在小于 Android Q 的设备上导致异常。
在用 "Android Version Check" if 语句包围后,图像直接保存在 Pictures
文件夹的根目录中。
再次感谢。
编辑 2
更改为:
uri = contentResolver.insert(contentUri, contentValues)
if (uri == null)
{
throw IOException("Failed to create new MediaStore record.")
}
val cursor = contentResolver.query(uri, null, null, null, null)
DatabaseUtils.dumpCursor(cursor)
cursor!!.close()
stream = contentResolver.openOutputStream(uri)
这是日志
I/System.out: >>>>> Dumping cursor android.content.ContentResolver$CursorWrapperInner@76da9d1
I/System.out: 0 {
I/System.out: _id=25417
I/System.out: _data=/storage/emulated/0/Pictures/1563640732667.jpg
I/System.out: _size=null
I/System.out: _display_name=Myimage
I/System.out: mime_type=image/png
I/System.out: title=1563640732667
I/System.out: date_added=1563640732
I/System.out: is_hdr=null
I/System.out: date_modified=null
I/System.out: description=null
I/System.out: picasa_id=null
I/System.out: isprivate=null
I/System.out: latitude=null
I/System.out: longitude=null
I/System.out: datetaken=null
I/System.out: orientation=null
I/System.out: mini_thumb_magic=null
I/System.out: bucket_id=-1617409521
I/System.out: bucket_display_name=Pictures
I/System.out: width=null
I/System.out: height=null
I/System.out: is_hw_privacy=null
I/System.out: hw_voice_offset=null
I/System.out: is_hw_favorite=null
I/System.out: hw_image_refocus=null
I/System.out: album_sort_index=null
I/System.out: bucket_display_name_alias=null
I/System.out: is_hw_burst=0
I/System.out: hw_rectify_offset=null
I/System.out: special_file_type=0
I/System.out: special_file_offset=null
I/System.out: cam_perception=null
I/System.out: cam_exif_flag=null
I/System.out: }
I/System.out: <<<<<
我注意到 title
与名称匹配,所以我尝试添加:
put(MediaStore.Images.ImageColumns.TITLE, name)
它仍然无效,这是新日志:
I/System.out: >>>>> Dumping cursor android.content.ContentResolver$CursorWrapperInner@51021a5
I/System.out: 0 {
I/System.out: _id=25418
I/System.out: _data=/storage/emulated/0/Pictures/1563640934803.jpg
I/System.out: _size=null
I/System.out: _display_name=Myimage
I/System.out: mime_type=image/png
I/System.out: title=Myimage
I/System.out: date_added=1563640934
I/System.out: is_hdr=null
I/System.out: date_modified=null
I/System.out: description=null
I/System.out: picasa_id=null
I/System.out: isprivate=null
I/System.out: latitude=null
I/System.out: longitude=null
I/System.out: datetaken=null
I/System.out: orientation=null
I/System.out: mini_thumb_magic=null
I/System.out: bucket_id=-1617409521
I/System.out: bucket_display_name=Pictures
I/System.out: width=null
I/System.out: height=null
I/System.out: is_hw_privacy=null
I/System.out: hw_voice_offset=null
I/System.out: is_hw_favorite=null
I/System.out: hw_image_refocus=null
I/System.out: album_sort_index=null
I/System.out: bucket_display_name_alias=null
I/System.out: is_hw_burst=0
I/System.out: hw_rectify_offset=null
I/System.out: special_file_type=0
I/System.out: special_file_offset=null
I/System.out: cam_perception=null
I/System.out: cam_exif_flag=null
I/System.out: }
I/System.out: <<<<<
而且我无法将 date_added
更改为名称。
并且 MediaStore.MediaColumns.DATA
已弃用。
提前致谢!
试试下一个方法。 Android Q(及以上)已经负责创建文件夹(如果它们不存在)。例子是hard-coded输出到DCIM文件夹。如果您需要 sub-folder,则将 sub-folder 名称附加为下一个:
final String relativeLocation = Environment.DIRECTORY_DCIM + File.separator + “YourSubforderName”;
考虑压缩格式应该与mime-type参数有关。例如,对于 JPEG 压缩格式,mime-type 将是“image/jpeg”,依此类推。可能您还想将压缩质量作为参数传递,在此示例中硬编码为 95。
Java:
@NonNull
public Uri saveBitmap(@NonNull final Context context, @NonNull final Bitmap bitmap,
@NonNull final Bitmap.CompressFormat format,
@NonNull final String mimeType,
@NonNull final String displayName) throws IOException {
final ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName);
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
final ContentResolver resolver = context.getContentResolver();
Uri uri = null;
try {
final Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
uri = resolver.insert(contentUri, values);
if (uri == null)
throw new IOException("Failed to create new MediaStore record.");
try (final OutputStream stream = resolver.openOutputStream(uri)) {
if (stream == null)
throw new IOException("Failed to open output stream.");
if (!bitmap.compress(format, 95, stream))
throw new IOException("Failed to save bitmap.");
}
return uri;
}
catch (IOException e) {
if (uri != null) {
// Don't leave an orphan entry in the MediaStore
resolver.delete(uri, null, null);
}
throw e;
}
}
科特林:
@Throws(IOException::class)
fun saveBitmap(
context: Context, bitmap: Bitmap, format: Bitmap.CompressFormat,
mimeType: String, displayName: String
): Uri {
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
}
val resolver = context.contentResolver
var uri: Uri? = null
try {
uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
?: throw IOException("Failed to create new MediaStore record.")
resolver.openOutputStream(uri)?.use {
if (!bitmap.compress(format, 95, it))
throw IOException("Failed to save bitmap.")
} ?: throw IOException("Failed to open output stream.")
return uri
} catch (e: IOException) {
uri?.let { orphanUri ->
// Don't leave an orphan entry in the MediaStore
resolver.delete(orphanUri, null, null)
}
throw e
}
}
Kotlin 变体,具有更实用的风格:
@Throws(IOException::class)
fun saveBitmap(
context: Context, bitmap: Bitmap, format: Bitmap.CompressFormat,
mimeType: String, displayName: String
): Uri {
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
}
var uri: Uri? = null
return runCatching {
with(context.contentResolver) {
insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)?.also {
uri = it // Keep uri reference so it can be removed on failure
openOutputStream(it)?.use { stream ->
if (!bitmap.compress(format, 95, stream))
throw IOException("Failed to save bitmap.")
} ?: throw IOException("Failed to open output stream.")
} ?: throw IOException("Failed to create new MediaStore record.")
}
}.getOrElse {
uri?.let { orphanUri ->
// Don't leave an orphan entry in the MediaStore
context.contentResolver.delete(orphanUri, null, null)
}
throw it
}
}
private void saveImage(Bitmap bitmap, @NonNull String name) throws IOException {
OutputStream fos;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name + ".jpg");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
fos = resolver.openOutputStream(Objects.requireNonNull(imageUri));
} else {
String imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString();
File image = new File(imagesDir, name + ".jpg");
fos = new FileOutputStream(image);
}
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
Objects.requireNonNull(fos).close();
}
图像将存储在图片文件夹@根级别
实时查看https://youtu.be/695HqaiwzQ0我创建了教程
这是我一直使用的。你可以试试。
private void saveImageToStorage() 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);
Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
imageOutStream = getContentResolver().openOutputStream(uri);
} else {
String imagePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString();
File image = new File(imagePath, "image_screenshotjpg");
imageOutStream = new FileOutputStream(image);
}
try {
bitmapObject.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream);
} finally {
imageOutStream.close();
}
}
如果有人在寻找如何将照片保存到 DCIM 文件夹中,以一种稍后会出现在 Google 照片中的方式:
(基于:https://github.com/yasirkula/UnityNativeGallery/blob/670d9e2b8328f7796dd95d29dd80fadd8935b804/JAR%20Source/NativeGallery.java#L73-L96)
ContentValue values = new ContentValues();
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
values.put(MediaStore.MediaColumns.DATE_TAKEN, System.currentTimeMillis());
values.put(MediaStore.MediaColumns.IS_PENDING, true);
Uri uri = context.getContentResolver().insert(externalContentUri, values);
if (uri != null) {
try {
if (WriteFileToStream(originalFile, context.getContentResolver().openOutputStream(uri))) {
values.put(MediaStore.MediaColumns.IS_PENDING, false);
context.getContentResolver().update(uri, values, null, null);
}
} catch (Exception e) {
context.getContentResolver().delete( uri, null, null );
}
}
其中 WriteFileToStream
是从文件复制到流的标准方法。
这是我的 2022 版本,此版本在 Emulator SDK 27 和 30 中也在三星 S22 上进行了测试 Phone。
TL:DR
SDK < 29 需要遵循代码here,成功拍照后几乎不需要添加代码。您可以在下面的 savePictureQ(...)
函数中看到
否则,如果您的 SDK >= 29,只需在 MediaStore.EXTRA_OUTPUT
extras from contentResolver.insert(...)
function
处传递 URI
因为 startActivityForResult(Intent)
已经使用 registerForActivityResult(...)
弃用了我的版本
private val cameraLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val name: String = viewModel.savePictureQ()
if (name != "") requireActivity().applicationContext.deleteFile(name)
val cr = requireContext().contentResolver
val uri = viewModel.getTargetUri()
if (uri != null) {
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val source = ImageDecoder.createSource(cr, uri)
ImageDecoder.decodeBitmap(source)
} else MediaStore.Images.Media.getBitmap(cr, uri)
val resized = Bitmap.createScaledBitmap(bitmap, 512, 512, true)
}
}
}
我在另一个名为 Repository.kt 的文件中调用 Intent,我还使用伪造的 viewModel 来调用 Repository 代码。
这是我调用 viewModel 代码的方式
private lateinit var viewModel: MenuViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
viewModel = MenuViewModel(Injection.provideRepository(requireContext()))
...
}
private fun permissionCheck() {
val granted = PackageManager.PERMISSION_GRANTED
val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (ActivityCompat.checkSelfPermission(
requireContext(),
permissions[0]
) != granted && ActivityCompat.checkSelfPermission(
requireContext(),
permissions[1]
) != granted && ActivityCompat.checkSelfPermission(
requireContext(),
permissions[2]
) != granted
) ActivityCompat.requestPermissions(
requireActivity(), permissions, MainActivity.REQUEST_CODE_PERMISSION
) else MainActivity.accepted = true
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (ActivityCompat.checkSelfPermission(
requireContext(),
permissions[2]
) != granted && ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.ACCESS_MEDIA_LOCATION
) != granted
) ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(permissions[2], Manifest.permission.ACCESS_MEDIA_LOCATION),
MainActivity.REQUEST_CODE_PERMISSION
) else MainActivity.accepted = true
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
bind.fromCamera.setOnClickListener {
permissionCheck()
if (`permission granted check`) {
viewModel.getCameraIntent(cameraLauncher)
}
}
...
}
在我的假 viewModel 中:
class MenuViewModel(private val repository: IRepository) {
fun getTargetUri() = repository.getTargetUri()
fun getCameraIntent(launcher: ActivityResultLauncher<Intent>) =
repository.createTakePictureIntent(launcher)
fun savePictureQ(): String = repository.savePictureQ()
}
在我的存储库代码中:
class Repository private constructor(private val context: Context) : IRepository {
companion object {
@Volatile
private var INSTANCE: IRepository? = null
fun getInstance(context: Context) = INSTANCE ?: synchronized(this) {
INSTANCE ?: Repository(context).apply { INSTANCE = this }
}
}
private var currentPath = ""
private var targetUri: Uri? = null
private fun createImageFile(): File { // create temporary file for SDK < 29
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(timestamp, ".jpg", storageDir)
.apply { currentPath = absolutePath }
}
override fun savePictureQ() : String { // Saving picture and added to Gallery for SDK < 29
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val f = File(currentPath)
val cr = context.contentResolver
val bitmap = BitmapFactory.decodeFile(currentPath)
val path = "${Environment.DIRECTORY_PICTURES}${File.separator}PoCkDetection"
val values = createContentValues(f.name, path)
var uri: Uri? = null
try {
uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)!!
val os = cr.openOutputStream(uri)
try {
val result = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os)
if (!result) throw Exception()
} catch (e: Exception) {
e.printStackTrace()
throw e
} finally {
os?.close()
targetUri = uri
}
f.delete()
if (f.exists()) {
f.canonicalFile.delete()
if (f.exists()) return f.name
}
} catch (e: Exception) {
e.printStackTrace()
uri?.let {
cr.delete(it, null, null)
}
}
}
return ""
}
override fun getTargetUri(): Uri? = targetUri
private fun createContentValues(title: String, path: String): ContentValues =
ContentValues().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.TITLE, "$title.jpg")
put(MediaStore.MediaColumns.DISPLAY_NAME, "$title.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis())
put(MediaStore.MediaColumns.DATE_TAKEN, System.currentTimeMillis())
put(MediaStore.MediaColumns.RELATIVE_PATH, path)
} else {
put(MediaStore.Images.Media.TITLE, "$title.jpg")
put(MediaStore.Images.Media.DISPLAY_NAME, "$title.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
}
}
override fun createTakePictureIntent(launcher: ActivityResultLauncher<Intent>) {
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
takePictureIntent.resolveActivity(context.packageManager).also {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val photoFile: File? = try {
createImageFile()
} catch (e: IOException) {
e.printStackTrace()
null
}
photoFile?.also {
val photoURI =
FileProvider.getUriForFile(context, "com.your.package.name", it)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
launcher.launch(takePictureIntent)
}
} else {
val timestamp =
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val path = "${Environment.DIRECTORY_PICTURES}${File.separator}PoCkDetection"
val values = createContentValues(timestamp, path)
val photoURI = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values
)
targetUri = photoURI
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
launcher.launch(takePictureIntent)
}
}
}
}
}
对于 SDK < 29,我遵循 Google Developer
中的代码
这是我的清单在遵循代码后的样子:
<application ...>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.your.package.name"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/camera_paths" />
</provider>
</application>
创建名为 xml 的新 res 文件夹,然后创建新的 xml 文件,确保名称与您放置在 <provider>
中的 <meta-data>
和该文件中的名称相同:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="camera_take"
path="Pictures" />
</paths>
这里有个link给新的Android问Scoped Storage.
根据this Android Developers Best Practices Blog, storing shared media files
(which is my case) should be done using the MediaStore API.
深入研究文档,我找不到相关函数。
这是我在 Kotlin 中的试用:
val bitmap = getImageBitmap() // I have a bitmap from a function or callback or whatever
val name = "example.png" // I have a name
val picturesDirectory = getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
// Make sure the directory "Android/data/com.mypackage.etc/files/Pictures" exists
if (!picturesDirectory.exists()) {
picturesDirectory.mkdirs()
}
try {
val out = FileOutputStream(File(picturesDirectory, name))
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
out.flush()
out.close()
} catch(e: Exception) {
// handle the error
}
结果是我的图片保存在这里 Android/data/com.mypackage.etc/files/Pictures/example.png
正如最佳实践博客中描述的 Storing app-internal files
我的问题是:
如何使用 MediaStore 保存图片API?
Java 中的答案同样可以接受。
提前致谢!
编辑
感谢 PerracoLabs
这真的很有帮助!
不过还有3分
这是我的代码:
val name = "Myimage"
val relativeLocation = Environment.DIRECTORY_PICTURES + File.pathSeparator + "AppName"
val contentValues = ContentValues().apply {
put(MediaStore.Images.ImageColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
// without this part causes "Failed to create new MediaStore record" exception to be invoked (uri is null below)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.ImageColumns.RELATIVE_PATH, relativeLocation)
}
}
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
var stream: OutputStream? = null
var uri: Uri? = null
try {
uri = contentResolver.insert(contentUri, contentValues)
if (uri == null)
{
throw IOException("Failed to create new MediaStore record.")
}
stream = contentResolver.openOutputStream(uri)
if (stream == null)
{
throw IOException("Failed to get output stream.")
}
if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream))
{
throw IOException("Failed to save bitmap.")
}
Snackbar.make(mCoordinator, R.string.image_saved_success, Snackbar.LENGTH_INDEFINITE).setAction("Open") {
val intent = Intent()
intent.type = "image/*"
intent.action = Intent.ACTION_VIEW
intent.data = contentUri
startActivity(Intent.createChooser(intent, "Select Gallery App"))
}.show()
} catch(e: IOException) {
if (uri != null)
{
contentResolver.delete(uri, null, null)
}
throw IOException(e)
}
finally {
stream?.close()
}
1- 保存的图像没有得到正确的名称"Myimage.png"
我尝试使用 "Myimage" 和 "Myimage.PNG" 但都没有用。
图片的名称总是由数字组成,例如:
1563468625314.jpg
这给我们带来了第二个问题:
2- 即使我以 png
.
jpg
问题不大。只是好奇为什么。
3- relativeLocation 位在小于 Android Q 的设备上导致异常。
在用 "Android Version Check" if 语句包围后,图像直接保存在 Pictures
文件夹的根目录中。
再次感谢。
编辑 2
更改为:
uri = contentResolver.insert(contentUri, contentValues)
if (uri == null)
{
throw IOException("Failed to create new MediaStore record.")
}
val cursor = contentResolver.query(uri, null, null, null, null)
DatabaseUtils.dumpCursor(cursor)
cursor!!.close()
stream = contentResolver.openOutputStream(uri)
这是日志
I/System.out: >>>>> Dumping cursor android.content.ContentResolver$CursorWrapperInner@76da9d1
I/System.out: 0 {
I/System.out: _id=25417
I/System.out: _data=/storage/emulated/0/Pictures/1563640732667.jpg
I/System.out: _size=null
I/System.out: _display_name=Myimage
I/System.out: mime_type=image/png
I/System.out: title=1563640732667
I/System.out: date_added=1563640732
I/System.out: is_hdr=null
I/System.out: date_modified=null
I/System.out: description=null
I/System.out: picasa_id=null
I/System.out: isprivate=null
I/System.out: latitude=null
I/System.out: longitude=null
I/System.out: datetaken=null
I/System.out: orientation=null
I/System.out: mini_thumb_magic=null
I/System.out: bucket_id=-1617409521
I/System.out: bucket_display_name=Pictures
I/System.out: width=null
I/System.out: height=null
I/System.out: is_hw_privacy=null
I/System.out: hw_voice_offset=null
I/System.out: is_hw_favorite=null
I/System.out: hw_image_refocus=null
I/System.out: album_sort_index=null
I/System.out: bucket_display_name_alias=null
I/System.out: is_hw_burst=0
I/System.out: hw_rectify_offset=null
I/System.out: special_file_type=0
I/System.out: special_file_offset=null
I/System.out: cam_perception=null
I/System.out: cam_exif_flag=null
I/System.out: }
I/System.out: <<<<<
我注意到 title
与名称匹配,所以我尝试添加:
put(MediaStore.Images.ImageColumns.TITLE, name)
它仍然无效,这是新日志:
I/System.out: >>>>> Dumping cursor android.content.ContentResolver$CursorWrapperInner@51021a5
I/System.out: 0 {
I/System.out: _id=25418
I/System.out: _data=/storage/emulated/0/Pictures/1563640934803.jpg
I/System.out: _size=null
I/System.out: _display_name=Myimage
I/System.out: mime_type=image/png
I/System.out: title=Myimage
I/System.out: date_added=1563640934
I/System.out: is_hdr=null
I/System.out: date_modified=null
I/System.out: description=null
I/System.out: picasa_id=null
I/System.out: isprivate=null
I/System.out: latitude=null
I/System.out: longitude=null
I/System.out: datetaken=null
I/System.out: orientation=null
I/System.out: mini_thumb_magic=null
I/System.out: bucket_id=-1617409521
I/System.out: bucket_display_name=Pictures
I/System.out: width=null
I/System.out: height=null
I/System.out: is_hw_privacy=null
I/System.out: hw_voice_offset=null
I/System.out: is_hw_favorite=null
I/System.out: hw_image_refocus=null
I/System.out: album_sort_index=null
I/System.out: bucket_display_name_alias=null
I/System.out: is_hw_burst=0
I/System.out: hw_rectify_offset=null
I/System.out: special_file_type=0
I/System.out: special_file_offset=null
I/System.out: cam_perception=null
I/System.out: cam_exif_flag=null
I/System.out: }
I/System.out: <<<<<
而且我无法将 date_added
更改为名称。
并且 MediaStore.MediaColumns.DATA
已弃用。
提前致谢!
试试下一个方法。 Android Q(及以上)已经负责创建文件夹(如果它们不存在)。例子是hard-coded输出到DCIM文件夹。如果您需要 sub-folder,则将 sub-folder 名称附加为下一个:
final String relativeLocation = Environment.DIRECTORY_DCIM + File.separator + “YourSubforderName”;
考虑压缩格式应该与mime-type参数有关。例如,对于 JPEG 压缩格式,mime-type 将是“image/jpeg”,依此类推。可能您还想将压缩质量作为参数传递,在此示例中硬编码为 95。
Java:
@NonNull
public Uri saveBitmap(@NonNull final Context context, @NonNull final Bitmap bitmap,
@NonNull final Bitmap.CompressFormat format,
@NonNull final String mimeType,
@NonNull final String displayName) throws IOException {
final ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName);
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
final ContentResolver resolver = context.getContentResolver();
Uri uri = null;
try {
final Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
uri = resolver.insert(contentUri, values);
if (uri == null)
throw new IOException("Failed to create new MediaStore record.");
try (final OutputStream stream = resolver.openOutputStream(uri)) {
if (stream == null)
throw new IOException("Failed to open output stream.");
if (!bitmap.compress(format, 95, stream))
throw new IOException("Failed to save bitmap.");
}
return uri;
}
catch (IOException e) {
if (uri != null) {
// Don't leave an orphan entry in the MediaStore
resolver.delete(uri, null, null);
}
throw e;
}
}
科特林:
@Throws(IOException::class)
fun saveBitmap(
context: Context, bitmap: Bitmap, format: Bitmap.CompressFormat,
mimeType: String, displayName: String
): Uri {
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
}
val resolver = context.contentResolver
var uri: Uri? = null
try {
uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
?: throw IOException("Failed to create new MediaStore record.")
resolver.openOutputStream(uri)?.use {
if (!bitmap.compress(format, 95, it))
throw IOException("Failed to save bitmap.")
} ?: throw IOException("Failed to open output stream.")
return uri
} catch (e: IOException) {
uri?.let { orphanUri ->
// Don't leave an orphan entry in the MediaStore
resolver.delete(orphanUri, null, null)
}
throw e
}
}
Kotlin 变体,具有更实用的风格:
@Throws(IOException::class)
fun saveBitmap(
context: Context, bitmap: Bitmap, format: Bitmap.CompressFormat,
mimeType: String, displayName: String
): Uri {
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
}
var uri: Uri? = null
return runCatching {
with(context.contentResolver) {
insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)?.also {
uri = it // Keep uri reference so it can be removed on failure
openOutputStream(it)?.use { stream ->
if (!bitmap.compress(format, 95, stream))
throw IOException("Failed to save bitmap.")
} ?: throw IOException("Failed to open output stream.")
} ?: throw IOException("Failed to create new MediaStore record.")
}
}.getOrElse {
uri?.let { orphanUri ->
// Don't leave an orphan entry in the MediaStore
context.contentResolver.delete(orphanUri, null, null)
}
throw it
}
}
private void saveImage(Bitmap bitmap, @NonNull String name) throws IOException {
OutputStream fos;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name + ".jpg");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
fos = resolver.openOutputStream(Objects.requireNonNull(imageUri));
} else {
String imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString();
File image = new File(imagesDir, name + ".jpg");
fos = new FileOutputStream(image);
}
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
Objects.requireNonNull(fos).close();
}
图像将存储在图片文件夹@根级别
实时查看https://youtu.be/695HqaiwzQ0我创建了教程
这是我一直使用的。你可以试试。
private void saveImageToStorage() 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);
Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
imageOutStream = getContentResolver().openOutputStream(uri);
} else {
String imagePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString();
File image = new File(imagePath, "image_screenshotjpg");
imageOutStream = new FileOutputStream(image);
}
try {
bitmapObject.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream);
} finally {
imageOutStream.close();
}
}
如果有人在寻找如何将照片保存到 DCIM 文件夹中,以一种稍后会出现在 Google 照片中的方式: (基于:https://github.com/yasirkula/UnityNativeGallery/blob/670d9e2b8328f7796dd95d29dd80fadd8935b804/JAR%20Source/NativeGallery.java#L73-L96)
ContentValue values = new ContentValues();
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
values.put(MediaStore.MediaColumns.DATE_TAKEN, System.currentTimeMillis());
values.put(MediaStore.MediaColumns.IS_PENDING, true);
Uri uri = context.getContentResolver().insert(externalContentUri, values);
if (uri != null) {
try {
if (WriteFileToStream(originalFile, context.getContentResolver().openOutputStream(uri))) {
values.put(MediaStore.MediaColumns.IS_PENDING, false);
context.getContentResolver().update(uri, values, null, null);
}
} catch (Exception e) {
context.getContentResolver().delete( uri, null, null );
}
}
其中 WriteFileToStream
是从文件复制到流的标准方法。
这是我的 2022 版本,此版本在 Emulator SDK 27 和 30 中也在三星 S22 上进行了测试 Phone。
TL:DR
SDK < 29 需要遵循代码here,成功拍照后几乎不需要添加代码。您可以在下面的 savePictureQ(...)
函数中看到
否则,如果您的 SDK >= 29,只需在 MediaStore.EXTRA_OUTPUT
extras from contentResolver.insert(...)
function
因为 startActivityForResult(Intent)
已经使用 registerForActivityResult(...)
private val cameraLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val name: String = viewModel.savePictureQ()
if (name != "") requireActivity().applicationContext.deleteFile(name)
val cr = requireContext().contentResolver
val uri = viewModel.getTargetUri()
if (uri != null) {
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val source = ImageDecoder.createSource(cr, uri)
ImageDecoder.decodeBitmap(source)
} else MediaStore.Images.Media.getBitmap(cr, uri)
val resized = Bitmap.createScaledBitmap(bitmap, 512, 512, true)
}
}
}
我在另一个名为 Repository.kt 的文件中调用 Intent,我还使用伪造的 viewModel 来调用 Repository 代码。 这是我调用 viewModel 代码的方式
private lateinit var viewModel: MenuViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
viewModel = MenuViewModel(Injection.provideRepository(requireContext()))
...
}
private fun permissionCheck() {
val granted = PackageManager.PERMISSION_GRANTED
val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (ActivityCompat.checkSelfPermission(
requireContext(),
permissions[0]
) != granted && ActivityCompat.checkSelfPermission(
requireContext(),
permissions[1]
) != granted && ActivityCompat.checkSelfPermission(
requireContext(),
permissions[2]
) != granted
) ActivityCompat.requestPermissions(
requireActivity(), permissions, MainActivity.REQUEST_CODE_PERMISSION
) else MainActivity.accepted = true
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (ActivityCompat.checkSelfPermission(
requireContext(),
permissions[2]
) != granted && ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.ACCESS_MEDIA_LOCATION
) != granted
) ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(permissions[2], Manifest.permission.ACCESS_MEDIA_LOCATION),
MainActivity.REQUEST_CODE_PERMISSION
) else MainActivity.accepted = true
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
bind.fromCamera.setOnClickListener {
permissionCheck()
if (`permission granted check`) {
viewModel.getCameraIntent(cameraLauncher)
}
}
...
}
在我的假 viewModel 中:
class MenuViewModel(private val repository: IRepository) {
fun getTargetUri() = repository.getTargetUri()
fun getCameraIntent(launcher: ActivityResultLauncher<Intent>) =
repository.createTakePictureIntent(launcher)
fun savePictureQ(): String = repository.savePictureQ()
}
在我的存储库代码中:
class Repository private constructor(private val context: Context) : IRepository {
companion object {
@Volatile
private var INSTANCE: IRepository? = null
fun getInstance(context: Context) = INSTANCE ?: synchronized(this) {
INSTANCE ?: Repository(context).apply { INSTANCE = this }
}
}
private var currentPath = ""
private var targetUri: Uri? = null
private fun createImageFile(): File { // create temporary file for SDK < 29
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(timestamp, ".jpg", storageDir)
.apply { currentPath = absolutePath }
}
override fun savePictureQ() : String { // Saving picture and added to Gallery for SDK < 29
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val f = File(currentPath)
val cr = context.contentResolver
val bitmap = BitmapFactory.decodeFile(currentPath)
val path = "${Environment.DIRECTORY_PICTURES}${File.separator}PoCkDetection"
val values = createContentValues(f.name, path)
var uri: Uri? = null
try {
uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)!!
val os = cr.openOutputStream(uri)
try {
val result = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os)
if (!result) throw Exception()
} catch (e: Exception) {
e.printStackTrace()
throw e
} finally {
os?.close()
targetUri = uri
}
f.delete()
if (f.exists()) {
f.canonicalFile.delete()
if (f.exists()) return f.name
}
} catch (e: Exception) {
e.printStackTrace()
uri?.let {
cr.delete(it, null, null)
}
}
}
return ""
}
override fun getTargetUri(): Uri? = targetUri
private fun createContentValues(title: String, path: String): ContentValues =
ContentValues().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.TITLE, "$title.jpg")
put(MediaStore.MediaColumns.DISPLAY_NAME, "$title.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis())
put(MediaStore.MediaColumns.DATE_TAKEN, System.currentTimeMillis())
put(MediaStore.MediaColumns.RELATIVE_PATH, path)
} else {
put(MediaStore.Images.Media.TITLE, "$title.jpg")
put(MediaStore.Images.Media.DISPLAY_NAME, "$title.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
}
}
override fun createTakePictureIntent(launcher: ActivityResultLauncher<Intent>) {
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
takePictureIntent.resolveActivity(context.packageManager).also {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val photoFile: File? = try {
createImageFile()
} catch (e: IOException) {
e.printStackTrace()
null
}
photoFile?.also {
val photoURI =
FileProvider.getUriForFile(context, "com.your.package.name", it)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
launcher.launch(takePictureIntent)
}
} else {
val timestamp =
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val path = "${Environment.DIRECTORY_PICTURES}${File.separator}PoCkDetection"
val values = createContentValues(timestamp, path)
val photoURI = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values
)
targetUri = photoURI
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
launcher.launch(takePictureIntent)
}
}
}
}
}
对于 SDK < 29,我遵循 Google Developer
中的代码这是我的清单在遵循代码后的样子:
<application ...>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.your.package.name"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/camera_paths" />
</provider>
</application>
创建名为 xml 的新 res 文件夹,然后创建新的 xml 文件,确保名称与您放置在 <provider>
中的 <meta-data>
和该文件中的名称相同:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="camera_take"
path="Pictures" />
</paths>