Android保存照片到相册

9,519 阅读5分钟

Android保存照片到相册

前言

随着Android系统的不断升级 从最初的第一个版本 更新到现在 Android 11.0 Beta都出炉了 Android 11.0也即将面试 系统的不断更新完善 用户体验也是蹭蹭蹭的 隐私安全方面也是越来越给力了 这对用户当然是一级棒 对于开发者 简直无力吐槽 因为碎片化问题 加上版本更新迭代 废弃淘汰一堆 API 脑瓜子疼。

正文

这几天刚刚需要保存视频到相册 发现 我去 以前的方法好像不太给力了,磨了 我好久。。因为我手机是Android 10 版本 从用户隐私加强了 最大的变化就是 存储权限

Android 10 在外部存储设备中为每个应用提供了一个“隔离存储沙盒”(例如 /sdcard)。任何其他应用都无法直接访问您应用的沙盒文件。由于文件是您应用的私有文件,因此您不再需要任何权限即可在外部存储设备中访问和保存自己的文件。此变更可让您更轻松地保证用户文件的隐私性,并有助于减少应用所需的权限数量。

所以就是说我们不能直接访问 根目录 了? 其实 在Android 10 上还不是完全杜绝你使用的 你还是可以兼容低版本 那怎么做呢

<application
 ...
 android:requestLegacyExternalStorage="true">
</application>

只需要在 你的AndroidManifest.xml 文件中 加入这行代码 意思就是申请旧版本的外部存储 那么你还是可与愉快玩耍的

可是旧版本的外部存储权限都已经废弃了 这样做是可以解决当前的问题 但是在Android 11 上 讲严格执行沙盒存储方式 也就是说 这样的代码在Android 11 上已经无法兼容了 并且的 10 的系统兼容也不够

撸码环节

那么开始撸代码把

/**
 *保存bitmap
 */
fun saveBitmap2Gallery(context: Context, bitmap: Bitmap): Boolean {
    val name=System.currentTimeMillis().toString()
    val photoPath=Environment.DIRECTORY_DCIM + "/Camera"
    val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME,name )
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            put(MediaStore.MediaColumns.RELATIVE_PATH, photoPath)//保存路径
            put(MediaStore.MediaColumns.IS_PENDING, true)
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            //返回出一个URI
            val insert = context.contentResolver.insert(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                contentValues
            ) ?: return false //为空的话 直接失败返回了

            //这个打开了输出流  直接保存图片就好了
            context.contentResolver.openOutputStream(insert).use {
                it ?: return false
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
            }
            return true
        } else {
            MediaStore.Images.Media.insertImage(context.contentResolver, bitmap, "title", "desc")
            return true
        }
    }

就上面的代码 可以直接保存了 如果不挑剔 应该是可以使用的了 但是发现一个问题 除了Android 10以上的 用 MediaStore 提示是废弃的 蒙蔽 为什么废弃呢 我们进入文档看看

inserting of images should be performed using {@link MediaColumns#IS_PENDING}, which offers richer control over lifecycle.

一看文档一脸懵逼 没看懂什么 各种查阅资料 百度一堆都是废弃的API。。。Android 这么难吗 保存个图片 都没有一个兼容的 完美的解决方案 或者 API 经过漫长的查询 种算看到眉目了 改良后的代码是这样的

 fun saveBitmap2Gallery2(context: Context, bitmap: Bitmap): Boolean {
        val name = System.currentTimeMillis().toString()
        val photoPath = Environment.DIRECTORY_DCIM + "/Camera"
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, name)
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.MediaColumns.RELATIVE_PATH, photoPath)//保存路径
                put(MediaStore.MediaColumns.IS_PENDING, true)
            }
        }
        val insert = context.contentResolver.insert(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues
        ) ?: return false //为空的话 直接失败返回了
        //这个打开了输出流  直接保存图片就好了
        context.contentResolver.openOutputStream(insert).use {
            it ?: return false
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            contentValues.put(MediaStore.MediaColumns.IS_PENDING, false)
        }
        return true
    }

是简单了好多 发现 在Android 10一下 只需要屏蔽 RELATIVE_PATHIS_PENDING 就可以了

但是发现一个问题 在Android 默认是保持在 /sdCard/Pictures/ 虽说也存放图片的地方 但是不是 /sdCard/DCIM/Camera小米 OV 等手机上 不能直接显示在照片里 而是在相册 中的 Pictures里 或者在全部照片也可以查到 这个问题我还是不知道怎么解决 因为 在使用 ContentValues 时 10.0以下的系统是不能设置路径的 那怎么办 可能还是的用废弃的 API 了 这里方法提供了 2种 自行选择最适合自己的 如果知道怎么保存到DCIM 欢迎评论区解答下

最后需要注意的是 在Android 10 中 保存到相册是不需要存储权限的 在6 - 9的版本中 需要存储权限

整合放出代码

object PhotoUtils {

    fun saveBitmap2Gallery(context: Context, bitmap: Bitmap): Boolean {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            //返回出一个URI
            val insert = context.contentResolver.insert(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                /*
                这里如果不写的话 默认是保存在 /sdCard/DCIM/Pictures
                 */
                ContentValues()//这里可以啥也不设置 保存图片默认就好了
            ) ?: return false //为空的话 直接失败返回了

            //这个打开了输出流  直接保存图片就好了
            context.contentResolver.openOutputStream(insert).use {
                it ?: return false
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
            }
            return true
        } else {
            MediaStore.Images.Media.insertImage(context.contentResolver, bitmap, "title", "desc")
            return true
        }
    }

    fun saveFile2Gallery(context: Context, url: String): Boolean {
        if (true) {
            //返回出一个URI
            val insert = context.contentResolver.insert(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                /*
                这里可以默认不写 默认保存在
                 */
                ContentValues()
            ) ?: return false //为空的话 直接失败返回了
            //这个打开了输出流  直接保存图片就好了
            context.contentResolver.openOutputStream(insert).use { os ->
                os ?: return false
                var x = download(url, os)
                return x
            }
            return false
        } else {
            val externalFilesDir =
                context.getExternalFilesDir(Environment.DIRECTORY_DCIM) ?: return false
            var name = "${System.currentTimeMillis()}.jpg"
            val file = File(externalFilesDir, name)
            //下载文件到应用目录
            download(url, file.outputStream())
            MediaStore.Images.Media.insertImage(
                context.contentResolver,
                file.absolutePath,
                name,
                "desc"
            )
            //刷新相册
            context.sendBroadcast(
                Intent(
                    Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
                    Uri.fromFile(File(file.getPath()))
                )
            )
            return true
        }
    }


    fun saveFile2Gallery2(context: Context, url: String): Boolean {
        val name = System.currentTimeMillis().toString()
        val photoPath = Environment.DIRECTORY_DCIM + "/Camera"
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, name)
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.MediaColumns.RELATIVE_PATH, photoPath)//保存路径
                put(MediaStore.MediaColumns.IS_PENDING, true)
            }
        }
        //返回出一个URI
        val insert = context.contentResolver.insert(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues
        ) ?: return false
        //这个打开了输出流  直接保存图片就好了
        context.contentResolver.openOutputStream(insert).use { os ->
            os ?: return false
            var x = download(url, os)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                contentValues.put(MediaStore.MediaColumns.IS_PENDING, false)
            }
            return x
        }
        return false
    }


    fun saveBitmap2Gallery2(context: Context, bitmap: Bitmap): Boolean {
        val name = System.currentTimeMillis().toString()
        val photoPath = Environment.DIRECTORY_DCIM + "/Camera"
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, name)
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.MediaColumns.RELATIVE_PATH, photoPath)//保存路径
                put(MediaStore.MediaColumns.IS_PENDING, true)
            }
        }
        val insert = context.contentResolver.insert(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues
        ) ?: return false //为空的话 直接失败返回了
        //这个打开了输出流  直接保存图片就好了
        context.contentResolver.openOutputStream(insert).use {
            it ?: return false
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            contentValues.put(MediaStore.MediaColumns.IS_PENDING, false)
        }
        return true
    }


    private fun download(url: String, os: OutputStream): Boolean {
        val url = URL(url)
        (url.openConnection() as HttpURLConnection).also { conn ->
            conn.requestMethod = "GET"
            conn.connectTimeout = 5 * 1000
            if (conn.responseCode == 200) {
                conn.inputStream.use { ins ->
                    val buf = ByteArray(2048)
                    var len: Int
                    while (ins.read(buf).also { len = it } != -1) {
                        os.write(buf, 0, len)
                    }
                    os.flush()
                }
                return true
            } else {
                return false
            }
        }
    }

}