Android actual combat scene - save the picture in WebView to the photo album

Last year, my colleague wrote a function of "save pictures to the album in H5". Although there is a general implementation idea, it is no problem to implement it, but I feel that my colleague has considered the problem very thoroughly. At that time, I wanted to learn it, but the project was too I can't catch up with it, so I have time now, so I'm ready to study hard

My memories about WebView~ Including getting started, optimizing the loading style, monitoring the loading status, processing the back button in each scene, the interaction process between the two ends, header, user-agent value transfer, common interactive problems, and full API integration

Business combat

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

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

Functional Analysis

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

Does H5 support long press event monitoring?

First of all, WebViewsupport by setOnLongClickListenerlistening to the long press event

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

How to judge the saved image when long press in H5? Instead of copywriting?

WebViewHitTestResultThe class is provided to facilitate obtaining the type results of user operations

insert image description here

It can be judged by type to know whether the user is manipulating the picture

    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")
    }

结合长按监听Unified writing together, you can directly obtain the operation results when the user presses for a long time

    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
    }

Saving pictures involves user privacy, and needs to be adapted to 6.0 dynamic permissions

About Android6.0 adaptation is a very old thing, which method to use can be defined by yourself (colleagues use Google's original permission request method)

Look: After the user refuses the authorization, when applying for permission again, he needs to jump to the application setting to enable authorization. In this regard, compatibility adaptation can also be done. The specific adaptation method is recorded in Android Compatibility Adaptation - Jump to the application permission setting page for different models

	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()
    }

The bottom is Contextextended Dialog函数, so you don’t need to pay too much attention to it, and the upper bullet box can customize the style (or use the original Dialog);

Monitoring Dialogused in the projectaddOnGlobalLayoutListener

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
}

How to determine the picture to be saved is Url? Or base64?

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

去年年初There was one when I was a child 交互需求是H5调用拍照、相册功能,然后将所选照片传给H5,这里我使用的方式就是将图片转为了base64串,然后传给H5用于展示, which involved some relevant knowledge. If you don’t understand it, you can learn it- Road to Android Advancement-Dual-terminal interaction to transfer Base64 pictures

Speaking of turning around, continue to look down

Because we have already judged that it must be the type of picture when we press and hold it, and then URLUtil.isValidUrl(extra)judge ;由此区分是图片url还是base64,然后将其转为bitmap用于存储

  • URLUtilis the original class Googleprovided by
  • extraIt is what we get when the user long presses
    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()internal implementation

insert image description here

save Picture

I used 协程it in my project 切换线程, and it can be implemented in different ways according to my own project scenario; the picture download method is used Glide框架, if you don’t know enough about the basics of Glide, you can go to my Glide Basics to simply consolidate it

Regarding the specific implementation saveToAlbumof the function , it will be declared in the extension function below

    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("已保存到系统相册")
    }

refresh gallery

In fact, the problem considered by my colleagues is quite perfect, and internal compatibility has also been made (it cannot be used directly, it needs to be combined with the extension function below)

/**
 * 插入图片到媒体库
 */
@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)
}

extension function

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

Bitmap extension functions

/**
 * 保存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 extension function

/**
 * 插入图片到媒体库
 */
@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 extension functions

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 extension function (image format)

@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 top-level file (covers extension functions used)

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
}

Project combat

Activity

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

Fragment

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

The original project used interface packaging, we only look at savePicturethe specific implementation

    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) }
    }
}

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

Guess you like

Origin blog.csdn.net/qq_20451879/article/details/128152152