国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Android實戰(zhàn)場景 - 保存WebView中的圖片到相冊

這篇具有很好參考價值的文章主要介紹了Android實戰(zhàn)場景 - 保存WebView中的圖片到相冊。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

去年同事寫了一個 “在H5中保存圖片到相冊” 的功能,雖然有大致實現(xiàn)思路,實現(xiàn)起來也沒問題,但是感覺同事考慮問題的很周全,當時候就想著去學習一下,但是項目太趕沒顧得上,索性現(xiàn)在有時間,準備好好學習一下

我那些關于WebView的回憶 ~ 包含入門使用、優(yōu)化加載樣式、監(jiān)聽加載狀態(tài)、各場景后退鍵處理、倆端交互流程、header、user-agent傳值、交互常見問題、較全API整合

  • Android 通過WebView與前端H5 雙端交互
  • Andorid與H5(JS)交互可能出錯的原因與解決方案

業(yè)務實戰(zhàn)

  • 場景:雙端交互傳遞圖片(Base64)
  • 場景:保存WebView中的圖片到相冊

業(yè)務場景:Android端使用WebView加載H5時,如果用戶長按其內部圖片,則彈框提示用戶可保存圖片

簡單說一下我的實現(xiàn)思路:首先監(jiān)聽WebView長按事件 → 判斷長按的內容是否為圖片類型 → 判斷圖片類型是url、還是base64 → 如果是url就下載圖片保存 → 如果是base64則轉Bitmap進行保存 → 保存成功刷新相冊圖庫

功能分析

Here:根據(jù)業(yè)務場景,來拆分一下具體實現(xiàn)中需要考慮的事情

H5中是否支持長按事件監(jiān)聽?

首先在 WebView支持通過setOnLongClickListener監(jiān)聽長按事件

    override fun setOnLongClickListener(l: OnLongClickListener?) {
        super.setOnLongClickListener(l)
    }
H5中長按時如何判斷保存的是圖片?而不是文案?

WebView 提供了 HitTestResult 類,方便獲取用戶操作時的類型結果

Android實戰(zhàn)場景 - 保存WebView中的圖片到相冊

可以通過類型判斷,得知用戶是否在操作圖片

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

結合長按監(jiān)聽統(tǒng)一寫在一起,可直接獲取用戶長按時的操作結果

    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動態(tài)權限

關于 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函數(shù) ,無需太過關注,上方彈框可自定義樣式(或用原始Dialog);

項目中 Dialog 用到的addOnGlobalLayoutListener 監(jiān)聽

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圖片

話說回頭,繼續(xù)往下看

因為在長按時我們已經判斷肯定是圖片類型了,接下來通過 URLUtil.isValidUrl(extra) 判斷其有效性;由此區(qū)分是圖片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() 內部實現(xiàn)

Android實戰(zhàn)場景 - 保存WebView中的圖片到相冊

保存圖片

我項目里用了協(xié)程切換線程,具體可根據(jù)自身項目場景使用不同方式去實現(xiàn);圖片下載方式用的是Glide框架,如果對 Glide 基礎方面,了解不足的話,可以去我的Glide基礎篇簡單鞏固下

關于 saveToAlbum 函數(shù)具體實現(xiàn),會在下方的擴展函數(shù)中聲明

    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("保存到系統(tǒng)相冊失敗")
            } finally {
                withContext(Dispatchers.Main) { loadingState(LoadingState.LoadingEnd) }
                dismissAllowingStateLoss()
            }
        }
    }

    private suspend fun saveToAlbum(bitmap: Bitmap?, fileName: String) {
        if (bitmap.isNull() || activity.isNull()) {
            return showToast("保存到系統(tǒng)相冊失敗")
        }
        val pictureUri = activity?.let { bitmap.saveToAlbum(it, fileName) }
        if (pictureUri == null) showToast("保存到系統(tǒng)相冊失敗") else showToast("已保存到系統(tǒng)相冊")
    }
刷新圖庫

其實同事考慮的問題也挺完善,內部也做了兼容(不可直接使用,需結合下方的擴展函數(shù))

/**
 * 插入圖片到媒體庫
 */
@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
        }
        // 文件路徑查重,重復的話在文件名后拼接數(shù)字
        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)
}

擴展函數(shù)

創(chuàng)建一個頂層文件 PictureSave,放置圖片相關的頂層函數(shù),更加方便調用

Bitmap 擴展函數(shù)
/**
 * 保存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 擴展函數(shù)
/**
 * 插入圖片到媒體庫
 */
@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
        }
        // 文件路徑查重,重復的話在文件名后拼接數(shù)字
        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 擴展函數(shù)
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狀態(tài),為0時其他應用才可見
        imageValues.put(MediaStore.Images.Media.IS_PENDING, 0)
        resolver.update(this, imageValues, null, null)
    }
}
String擴展函數(shù)(圖片格式)
@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 頂層文件(涵蓋所用擴展函數(shù))
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狀態(tài),為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
        }
        // 文件路徑查重,重復的話在文件名后拼接數(shù)字
        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
}

項目實戰(zhàn)

Activity

    webView.setLongClickListener {
        ComponentService.service?.savePicture(this, it)// 彈出保存圖片的對話框
    }

Fragment

    webView.setLongClickListener {
        activity?.run { ComponentService.service?.savePicture(this, it) }// 彈出保存圖片的對話框
    }

原項目中使用了接口包裝,我們只看 savePicture 具體實現(xiàn)

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

因為項目用的MVI框架,可自行忽略部分實現(xiàn),主要關注自己想看的...

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("保存到系統(tǒng)相冊失敗")
            } finally {
                withContext(Dispatchers.Main) { loadingState(LoadingState.LoadingEnd) }
                dismissAllowingStateLoss()
            }
        }
    }

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

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

全都過一次后,也是收獲滿滿,爭取明天再進一步,加油 > < ~文章來源地址http://www.zghlxwxcb.cn/news/detail-408875.html

到了這里,關于Android實戰(zhàn)場景 - 保存WebView中的圖片到相冊的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!

本文來自互聯(lián)網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如若轉載,請注明出處: 如若內容造成侵權/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包