Android实战场景 - 保存WebView中的图片到相册

去年同事写了一个 “在H5中保存图片到相册” 的功能,虽然有大致实现思路,实现起来也没问题,但是感觉同事考虑问题的很周全,当时候就想着去学习一下,但是项目太赶没顾得上,索性现在有时间,准备好好学习一下

我那些关于WebView的回忆 ~ 包含入门使用、优化加载样式、监听加载状态、各场景后退键处理、俩端交互流程、header、user-agent传值、交互常见问题、较全API整合

业务实战

业务场景:Android端使用WebView加载H5时,如果用户长按其内部图片,则弹框提示用户可保存图片

简单说一下我的实现思路:首先监听WebView长按事件 → 判断长按的内容是否为图片类型 → 判断图片类型是url、还是base64 → 如果是url就下载图片保存 → 如果是base64则转Bitmap进行保存 → 保存成功刷新相册图库

功能分析

Here:根据业务场景,来拆分一下具体实现中需要考虑的事情

H5中是否支持长按事件监听?

首先在 WebView支持通过setOnLongClickListener监听长按事件

    override fun setOnLongClickListener(l: OnLongClickListener?) {
    
    
        super.setOnLongClickListener(l)
    }

H5中长按时如何判断保存的是图片?而不是文案?

WebView 提供了 HitTestResult 类,方便获取用户操作时的类型结果

在这里插入图片描述

扫描二维码关注公众号,回复: 14808630 查看本文章

可以通过类型判断,得知用户是否在操作图片

    val hitTestResult: HitTestResult = hitTestResult
    // 如果是图片类型或者是带有图片链接的类型
    if (hitTestResult.type == HitTestResult.IMAGE_TYPE ||
    hitTestResult.type == HitTestResult.SRC_IMAGE_ANCHOR_TYPE
    ) {
    
    
        val extra = hitTestResult.extra
        Timber.e("图片地址或base64:$extra")
    }

结合长按监听统一写在一起,可直接获取用户长按时的操作结果

    setOnLongClickListener {
    
    
        val hitTestResult: HitTestResult = hitTestResult
        // 如果是图片类型或者是带有图片链接的类型
        if (hitTestResult.type == HitTestResult.IMAGE_TYPE ||
            hitTestResult.type == HitTestResult.SRC_IMAGE_ANCHOR_TYPE
        ) {
    
    
            val extra = hitTestResult.extra
            Timber.e("图片地址或base64:$extra")
            longClickListener?.invoke(extra)
        }
        true
    }

保存图片涉及用户隐私,需适配6.0动态权限

关于 Android6.0适配 是很老的东西了,具体使用哪种方式可自行定义(同事使用的是Google原始权限请求方式)

Look:当用户拒绝授权后,再次申请权限时需跳转应用设置内开启授权,关于这方面也可做兼容适配,具体适配方式记录于 Android兼容适配 - 不同机型跳转应用权限设置页面

	private val permission by lazy {
    
     Manifest.permission.WRITE_EXTERNAL_STORAGE }
    private val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
    
    
        if (it) return@registerForActivityResult savePicture()
        if (shouldShowRequestPermissionRationale(permission)) {
    
    
            activity?.alertDialog {
    
    
                setTitle("权限申请")
                setMessage("我们需要获取写文件权限, 否则您将无法正常使用图片保存功能")
                setNegativeButton("取消")
                setPositiveButton("申请授权") {
    
     checkPermission() }
            }
        } else {
    
    
            activity?.alertDialog {
    
    
                setTitle("权限申请")
                setMessage("由于无法获取读文件权限, 无法正常使用图片保存功能, 请开启权限后再使用。\n\n设置路径: 应用管理->华安基金->权限")
                setNegativeButton("取消")
                setPositiveButton("去设置") {
    
    
                    activity?.let {
    
     context -> PermissionPageUtils(context).jumpPermissionPage() }
                }
            }
        }
    }

    private fun checkPermission() {
    
    
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                && ContextCompat.checkSelfPermission(AppContext, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
    
     // 无权限
            return permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
        }
        savePicture()
    }

下方为 Context 扩展出来的 Dialog函数 ,无需太过关注,上方弹框可自定义样式(或用原始Dialog);

项目中 Dialog 用到的addOnGlobalLayoutListener 监听

fun Context.alertDialog(builder: AppDialogBuilder.() -> Unit): AppDialogBuilder {
    
    
    val alertDialogUi = AlertDialogUi(this)
    alertDialogUi.viewTreeObserver.addOnGlobalLayoutListener {
    
    
        if (alertDialogUi.height > AppContext.screenHeight / 3 * 2) {
    
    
            alertDialogUi.updateLayoutParams<ViewGroup.LayoutParams> {
    
    
                height = AppContext.screenHeight / 3 * 2 - dip(20)
            }
        }
    }
    val alertDialogBuilder = AlertDialog.Builder(this).setCancelable(false).setView(alertDialogUi)
    val appDialogBuilder = AppDialogBuilder(alertDialogUi, alertDialogBuilder)
    appDialogBuilder.builder()
    appDialogBuilder.show()
    return appDialogBuilder
}

如何确定要保存的图片是Url?还是base64?

在双端交互时涉及到图片展示、保存相关需求的话,一般会有俩种传递方式,一种为图片的url地址,一种为base64串;

去年年初的时候有一个交互需求是H5调用拍照、相册功能,然后将所选照片传给H5,这里我使用的方式就是将图片转为了base64串,然后传给H5用于展示,其中涉及到了一些相关知识,不了解的话,可以去学习一下 - Android进阶之路 - 双端交互之传递Base64图片

话说回头,继续往下看

因为在长按时我们已经判断肯定是图片类型了,接下来通过 URLUtil.isValidUrl(extra) 判断其有效性;由此区分是图片url还是base64,然后将其转为bitmap用于存储

  • URLUtilGoogle 提供的原始类
  • extra 是用户长按时我们获取到的
    val bitmap = if (URLUtil.isValidUrl(extra)) {
    
    
        activity?.let {
    
     Glide.with(it).asBitmap().load(extra).submit().get() }
    } else {
    
    
        val base64 = extra?.split(",")?.getOrNull(1) ?: extra
        val decode = Base64.decode(base64, Base64.NO_WRAP)
        BitmapFactory.decodeByteArray(decode, 0, decode.size)
    }

URLUtil.isValidUrl() 内部实现

在这里插入图片描述

保存图片

我项目里用了协程切换线程,具体可根据自身项目场景使用不同方式去实现;图片下载方式用的是Glide框架如果对 Glide 基础方面,了解不足的话,可以去我的Glide基础篇简单巩固下

关于 saveToAlbum 函数具体实现,会在下方的扩展函数中声明

    private fun savePicture() {
    
    
        lifecycleScope.launch(Dispatchers.IO) {
    
    
            try {
    
    
                withContext(Dispatchers.Main) {
    
     loadingState(LoadingState.LoadingStart) }
                val bitmap = if (URLUtil.isValidUrl(extra)) {
    
    
                    activity?.let {
    
     Glide.with(it).asBitmap().load(extra).submit().get() }
                } else {
    
    
                    val base64 = extra?.split(",")?.getOrNull(1) ?: extra
                    val decode = Base64.decode(base64, Base64.NO_WRAP)
                    BitmapFactory.decodeByteArray(decode, 0, decode.size)
                }
                Timber.d("保存相册图片大小:${
      
      bitmap?.byteCount}")
                saveToAlbum(bitmap, "ha_${
      
      System.currentTimeMillis()}.png")
            } catch (throwable: Throwable) {
    
    
                Timber.e(throwable)
                showToast("保存到系统相册失败")
            } finally {
    
    
                withContext(Dispatchers.Main) {
    
     loadingState(LoadingState.LoadingEnd) }
                dismissAllowingStateLoss()
            }
        }
    }

    private suspend fun saveToAlbum(bitmap: Bitmap?, fileName: String) {
    
    
        if (bitmap.isNull() || activity.isNull()) {
    
    
            return showToast("保存到系统相册失败")
        }
        val pictureUri = activity?.let {
    
     bitmap.saveToAlbum(it, fileName) }
        if (pictureUri == null) showToast("保存到系统相册失败") else showToast("已保存到系统相册")
    }

刷新图库

其实同事考虑的问题也挺完善,内部也做了兼容(不可直接使用,需结合下方的扩展函数)

/**
 * 插入图片到媒体库
 */
@Suppress("DEPRECATION")
private fun ContentResolver.insertMediaImage(fileName: String, outputFileTaker: OutputFileTaker? = null): Uri? {
    
    
    // 图片信息
    val imageValues = ContentValues().apply {
    
    
        val mimeType = fileName.getMimeType()
        if (mimeType != null) {
    
    
            put(MediaStore.Images.Media.MIME_TYPE, mimeType)
        }
        val date = System.currentTimeMillis() / 1000
        put(MediaStore.Images.Media.DATE_ADDED, date)
        put(MediaStore.Images.Media.DATE_MODIFIED, date)
    }
    // 保存的位置
    val collection: Uri
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    
    
        imageValues.apply {
    
    
            put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
            put(MediaStore.Images.Media.IS_PENDING, 1)
        }
        collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
        // 高版本不用查重直接插入,会自动重命名
    } else {
    
    
        // 老版本
        val pictures = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
        if (!pictures.exists() && !pictures.mkdirs()) {
    
    
            Timber.e("save: error: can't create Pictures directory")
            return null
        }
        // 文件路径查重,重复的话在文件名后拼接数字
        var imageFile = File(pictures, fileName)
        val fileNameWithoutExtension = imageFile.nameWithoutExtension
        val fileExtension = imageFile.extension
        var queryUri = this.queryMediaImage28(imageFile.absolutePath)
        var suffix = 1
        while (queryUri != null) {
    
    
            val newName = fileNameWithoutExtension + "(${
      
      suffix++})." + fileExtension
            imageFile = File(pictures, newName)
            queryUri = this.queryMediaImage28(imageFile.absolutePath)
        }
        imageValues.apply {
    
    
            put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name)
            Timber.e("save file: $imageFile.absolutePath") // 保存路径
            put(MediaStore.Images.Media.DATA, imageFile.absolutePath)
        }
        outputFileTaker?.file = imageFile// 回传文件路径,用于设置文件大小
        collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    }
    // 插入图片信息
    return this.insert(collection, imageValues)
}

扩展函数

创建一个顶层文件 PictureSave,放置图片相关的顶层函数,更加方便调用

Bitmap 扩展函数

/**
 * 保存Bitmap到相册的Pictures文件夹
 *
 * 官网文档:https://developer.android.google.cn/training/data-storage/shared/media
 *
 * @param context 上下文
 * @param fileName 文件名。 需要携带后缀
 * @param quality 质量(图片质量决定了图片大小)
 */
internal fun Bitmap.saveToAlbum(context: Context, fileName: String, quality: Int = 75): Uri? {
    
    
    // 插入图片信息
    val resolver = context.contentResolver
    val outputFile = OutputFileTaker()
    val imageUri = resolver.insertMediaImage(fileName, outputFile)
    if (imageUri == null) {
    
    
        Timber.e("insert: error: uri == null")
        return null
    }
    // 保存图片
    (imageUri.outputStream(resolver) ?: return null).use {
    
    
        val format = fileName.getBitmapFormat()
        this@saveToAlbum.compress(format, quality, it)
        imageUri.finishPending(context, resolver, outputFile.file)
    }
    return imageUri
}

private fun Uri.outputStream(resolver: ContentResolver): OutputStream? {
    
    
    return try {
    
    
        resolver.openOutputStream(this)
    } catch (e: FileNotFoundException) {
    
    
        Timber.e("save: open stream error: $e")
        null
    }
}

ContentResolver 扩展函数

/**
 * 插入图片到媒体库
 */
@Suppress("DEPRECATION")
private fun ContentResolver.insertMediaImage(fileName: String, outputFileTaker: OutputFileTaker? = null): Uri? {
    
    
    // 图片信息
    val imageValues = ContentValues().apply {
    
    
        val mimeType = fileName.getMimeType()
        if (mimeType != null) {
    
    
            put(MediaStore.Images.Media.MIME_TYPE, mimeType)
        }
        val date = System.currentTimeMillis() / 1000
        put(MediaStore.Images.Media.DATE_ADDED, date)
        put(MediaStore.Images.Media.DATE_MODIFIED, date)
    }
    // 保存的位置
    val collection: Uri
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    
    
        imageValues.apply {
    
    
            put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
            put(MediaStore.Images.Media.IS_PENDING, 1)
        }
        collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
        // 高版本不用查重直接插入,会自动重命名
    } else {
    
    
        // 老版本
        val pictures = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
        if (!pictures.exists() && !pictures.mkdirs()) {
    
    
            Timber.e("save: error: can't create Pictures directory")
            return null
        }
        // 文件路径查重,重复的话在文件名后拼接数字
        var imageFile = File(pictures, fileName)
        val fileNameWithoutExtension = imageFile.nameWithoutExtension
        val fileExtension = imageFile.extension
        var queryUri = this.queryMediaImage28(imageFile.absolutePath)
        var suffix = 1
        while (queryUri != null) {
    
    
            val newName = fileNameWithoutExtension + "(${
      
      suffix++})." + fileExtension
            imageFile = File(pictures, newName)
            queryUri = this.queryMediaImage28(imageFile.absolutePath)
        }
        imageValues.apply {
    
    
            put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name)
            Timber.e("save file: $imageFile.absolutePath") // 保存路径
            put(MediaStore.Images.Media.DATA, imageFile.absolutePath)
        }
        outputFileTaker?.file = imageFile// 回传文件路径,用于设置文件大小
        collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    }
    // 插入图片信息
    return this.insert(collection, imageValues)
}

/**
 * Android Q以下版本,查询媒体库中当前路径是否存在
 * @return Uri 返回null时说明不存在,可以进行图片插入逻辑
 */
@Suppress("DEPRECATION")
private fun ContentResolver.queryMediaImage28(imagePath: String): Uri? {
    
    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return null
    val imageFile = File(imagePath)
    if (imageFile.canRead() && imageFile.exists()) {
    
    
        Timber.e("query: path: $imagePath exists")
        // 文件已存在,返回一个file://xxx的uri
        return Uri.fromFile(imageFile)
    }
    // 保存的位置
    val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI

    // 查询是否已经存在相同图片
    val query = this.query(
            collection,
            arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA),
            "${
      
      MediaStore.Images.Media.DATA} == ?",
            arrayOf(imagePath), null
    )
    query?.use {
    
    
        while (it.moveToNext()) {
    
    
            val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
            val id = it.getLong(idColumn)
            return ContentUris.withAppendedId(collection, id)
        }
    }
    return null
}

Uri 扩展函数

private fun Uri.outputStream(resolver: ContentResolver): OutputStream? {
    
    
    return try {
    
    
        resolver.openOutputStream(this)
    } catch (e: FileNotFoundException) {
    
    
        Timber.e("save: open stream error: $e")
        null
    }
}

@Suppress("DEPRECATION")
private fun Uri.finishPending(context: Context, resolver: ContentResolver, outputFile: File?) {
    
    
    val imageValues = ContentValues()
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
    
    
        if (outputFile != null) {
    
    
            imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length())
        }
        resolver.update(this, imageValues, null, null)
        // 通知媒体库更新
        val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this)
        context.sendBroadcast(intent)
    } else {
    
    
        // Android Q添加了IS_PENDING状态,为0时其他应用才可见
        imageValues.put(MediaStore.Images.Media.IS_PENDING, 0)
        resolver.update(this, imageValues, null, null)
    }
}

String扩展函数(图片格式)

@Suppress("DEPRECATION")
private fun String.getBitmapFormat(): Bitmap.CompressFormat {
    
    
    val fileName = this.lowercase()
    return when {
    
    
        fileName.endsWith(".png") -> Bitmap.CompressFormat.PNG
        fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> Bitmap.CompressFormat.JPEG
        fileName.endsWith(".webp") -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
            Bitmap.CompressFormat.WEBP_LOSSLESS else Bitmap.CompressFormat.WEBP
        else -> Bitmap.CompressFormat.PNG
    }
}

private fun String.getMimeType(): String? {
    
    
    val fileName = this.lowercase()
    return when {
    
    
        fileName.endsWith(".png") -> "image/png"
        fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> "image/jpeg"
        fileName.endsWith(".webp") -> "image/webp"
        fileName.endsWith(".gif") -> "image/gif"
        else -> null
    }
}

PictureSave 顶层文件(涵盖所用扩展函数)

package xxx

import android.content.*
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
import java.io.OutputStream

private class OutputFileTaker(var file: File? = null)

/**
 * 保存Bitmap到相册的Pictures文件夹
 *
 * https://developer.android.google.cn/training/data-storage/shared/media
 *
 * @param context 上下文
 * @param fileName 文件名。 需要携带后缀
 * @param quality 质量
 */
internal fun Bitmap.saveToAlbum(context: Context, fileName: String, quality: Int = 75): Uri? {
    
    
    // 插入图片信息
    val resolver = context.contentResolver
    val outputFile = OutputFileTaker()
    val imageUri = resolver.insertMediaImage(fileName, outputFile)
    if (imageUri == null) {
    
    
        Timber.e("insert: error: uri == null")
        return null
    }
    // 保存图片
    (imageUri.outputStream(resolver) ?: return null).use {
    
    
        val format = fileName.getBitmapFormat()
        this@saveToAlbum.compress(format, quality, it)
        imageUri.finishPending(context, resolver, outputFile.file)
    }
    return imageUri
}

private fun Uri.outputStream(resolver: ContentResolver): OutputStream? {
    
    
    return try {
    
    
        resolver.openOutputStream(this)
    } catch (e: FileNotFoundException) {
    
    
        Timber.e("save: open stream error: $e")
        null
    }
}

@Suppress("DEPRECATION")
private fun Uri.finishPending(context: Context, resolver: ContentResolver, outputFile: File?) {
    
    
    val imageValues = ContentValues()
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
    
    
        if (outputFile != null) {
    
    
            imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length())
        }
        resolver.update(this, imageValues, null, null)
        // 通知媒体库更新
        val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this)
        context.sendBroadcast(intent)
    } else {
    
    
        // Android Q添加了IS_PENDING状态,为0时其他应用才可见
        imageValues.put(MediaStore.Images.Media.IS_PENDING, 0)
        resolver.update(this, imageValues, null, null)
    }
}

@Suppress("DEPRECATION")
private fun String.getBitmapFormat(): Bitmap.CompressFormat {
    
    
    val fileName = this.lowercase()
    return when {
    
    
        fileName.endsWith(".png") -> Bitmap.CompressFormat.PNG
        fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> Bitmap.CompressFormat.JPEG
        fileName.endsWith(".webp") -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
            Bitmap.CompressFormat.WEBP_LOSSLESS else Bitmap.CompressFormat.WEBP
        else -> Bitmap.CompressFormat.PNG
    }
}

private fun String.getMimeType(): String? {
    
    
    val fileName = this.lowercase()
    return when {
    
    
        fileName.endsWith(".png") -> "image/png"
        fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> "image/jpeg"
        fileName.endsWith(".webp") -> "image/webp"
        fileName.endsWith(".gif") -> "image/gif"
        else -> null
    }
}

/**
 * 插入图片到媒体库
 */
@Suppress("DEPRECATION")
private fun ContentResolver.insertMediaImage(fileName: String, outputFileTaker: OutputFileTaker? = null): Uri? {
    
    
    // 图片信息
    val imageValues = ContentValues().apply {
    
    
        val mimeType = fileName.getMimeType()
        if (mimeType != null) {
    
    
            put(MediaStore.Images.Media.MIME_TYPE, mimeType)
        }
        val date = System.currentTimeMillis() / 1000
        put(MediaStore.Images.Media.DATE_ADDED, date)
        put(MediaStore.Images.Media.DATE_MODIFIED, date)
    }
    // 保存的位置
    val collection: Uri
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    
    
        imageValues.apply {
    
    
            put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
            put(MediaStore.Images.Media.IS_PENDING, 1)
        }
        collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
        // 高版本不用查重直接插入,会自动重命名
    } else {
    
    
        // 老版本
        val pictures = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
        if (!pictures.exists() && !pictures.mkdirs()) {
    
    
            Timber.e("save: error: can't create Pictures directory")
            return null
        }
        // 文件路径查重,重复的话在文件名后拼接数字
        var imageFile = File(pictures, fileName)
        val fileNameWithoutExtension = imageFile.nameWithoutExtension
        val fileExtension = imageFile.extension
        var queryUri = this.queryMediaImage28(imageFile.absolutePath)
        var suffix = 1
        while (queryUri != null) {
    
    
            val newName = fileNameWithoutExtension + "(${
      
      suffix++})." + fileExtension
            imageFile = File(pictures, newName)
            queryUri = this.queryMediaImage28(imageFile.absolutePath)
        }
        imageValues.apply {
    
    
            put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name)
            Timber.e("save file: $imageFile.absolutePath") // 保存路径
            put(MediaStore.Images.Media.DATA, imageFile.absolutePath)
        }
        outputFileTaker?.file = imageFile// 回传文件路径,用于设置文件大小
        collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    }
    // 插入图片信息
    return this.insert(collection, imageValues)
}

/**
 * Android Q以下版本,查询媒体库中当前路径是否存在
 * @return Uri 返回null时说明不存在,可以进行图片插入逻辑
 */
@Suppress("DEPRECATION")
private fun ContentResolver.queryMediaImage28(imagePath: String): Uri? {
    
    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return null
    val imageFile = File(imagePath)
    if (imageFile.canRead() && imageFile.exists()) {
    
    
        Timber.e("query: path: $imagePath exists")
        // 文件已存在,返回一个file://xxx的uri
        return Uri.fromFile(imageFile)
    }
    // 保存的位置
    val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI

    // 查询是否已经存在相同图片
    val query = this.query(
            collection,
            arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA),
            "${
      
      MediaStore.Images.Media.DATA} == ?",
            arrayOf(imagePath), null
    )
    query?.use {
    
    
        while (it.moveToNext()) {
    
    
            val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
            val id = it.getLong(idColumn)
            return ContentUris.withAppendedId(collection, id)
        }
    }
    return null
}

项目实战

Activity

    webView.setLongClickListener {
    
    
        ComponentService.service?.savePicture(this, it)// 弹出保存图片的对话框
    }

Fragment

    webView.setLongClickListener {
    
    
        activity?.run {
    
     ComponentService.service?.savePicture(this, it) }// 弹出保存图片的对话框
    }

原项目中使用了接口包装,我们只看 savePicture 具体实现

    override fun savePicture(activity: FragmentActivity, extra: String?) {
    
    
        if (extra.isNullOrEmpty()) return
        activity.currentFocus?.clearFocus()
        activity.showAsync({
    
     PictureSaveBottomSheetDialogFragment() }, tag = "PictureSaveBottomSheetDialogFragment") {
    
    
            this.extra = extra
        }
    }

因为项目用的MVI框架,可自行忽略部分实现,主要关注自己想看的...

PictureSaveBottomSheetDialogFragment

internal class PictureSaveBottomSheetDialogFragment : BaseMavericksBottomSheetDialogFragment() {
    
    

    private val permission by lazy {
    
     Manifest.permission.WRITE_EXTERNAL_STORAGE }
    var extra: String? = null

    private val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
    
    
        if (it) return@registerForActivityResult savePicture()
        if (shouldShowRequestPermissionRationale(permission)) {
    
    
            activity?.alertDialog {
    
    
                setTitle("权限申请")
                setMessage("我们需要获取写文件权限, 否则您将无法正常使用图片保存功能")
                setNegativeButton("取消")
                setPositiveButton("申请授权") {
    
     checkPermission() }
            }
        } else {
    
    
            activity?.alertDialog {
    
    
                setTitle("权限申请")
                setMessage("由于无法获取读文件权限, 无法正常使用图片保存功能, 请开启权限后再使用。\n\n设置路径: 应用管理->华安基金->权限")
                setNegativeButton("取消")
                setPositiveButton("去设置") {
    
    
                    activity?.let {
    
     context -> PermissionPageUtils(context).jumpPermissionPage() }
                }
            }
        }
    }

    override fun settingHeader(titleBar: TitleBar) {
    
    
        titleBar.isGone = true
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    
    
        super.onViewCreated(view, savedInstanceState)
        lifecycleScope.launchWhenResumed {
    
     postInvalidate() }
    }

    override fun epoxyController() = simpleController {
    
    
        pictureSaveUi {
    
    
            id("pictureSaveUi")
            cancelClick {
    
     _ -> dismissAllowingStateLoss() }
            saveClick {
    
     _ -> checkPermission() }
        }
    }

    private fun checkPermission() {
    
    
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                && ContextCompat.checkSelfPermission(AppContext, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
    
     // 无权限
            return permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
        }
        savePicture()
    }

    private fun savePicture() {
    
    
        lifecycleScope.launch(Dispatchers.IO) {
    
    
            try {
    
    
                withContext(Dispatchers.Main) {
    
     loadingState(LoadingState.LoadingStart) }
                val bitmap = if (URLUtil.isValidUrl(extra)) {
    
    
                    activity?.let {
    
     Glide.with(it).asBitmap().load(extra).submit().get() }
                } else {
    
    
                    val base64 = extra?.split(",")?.getOrNull(1) ?: extra
                    val decode = Base64.decode(base64, Base64.NO_WRAP)
                    BitmapFactory.decodeByteArray(decode, 0, decode.size)
                }
                Timber.d("保存相册图片大小:${
      
      bitmap?.byteCount}")
                saveToAlbum(bitmap, "ha_${
      
      System.currentTimeMillis()}.png")
            } catch (throwable: Throwable) {
    
    
                Timber.e(throwable)
                showToast("保存到系统相册失败")
            } finally {
    
    
                withContext(Dispatchers.Main) {
    
     loadingState(LoadingState.LoadingEnd) }
                dismissAllowingStateLoss()
            }
        }
    }

    private suspend fun saveToAlbum(bitmap: Bitmap?, fileName: String) {
    
    
        if (bitmap.isNull() || activity.isNull()) {
    
    
            return showToast("保存到系统相册失败")
        }
        val pictureUri = activity?.let {
    
     bitmap.saveToAlbum(it, fileName) }
        if (pictureUri == null) showToast("保存到系统相册失败") else showToast("已保存到系统相册")
    }

    private suspend fun showToast(message: String) {
    
    
        withContext(Dispatchers.Main) {
    
     ToastUtils.showToast(message) }
    }
}

全都过一次后,也是收获满满,争取明天再进一步,加油 > < ~

猜你喜欢

转载自blog.csdn.net/qq_20451879/article/details/128152152