Android实战场景 - 保存WebView中的图片到相册
创始人
2024-05-23 12:52:28
0

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

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

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

      • 功能分析
        • H5中是否支持长按事件监听?
        • H5中长按时如何判断保存的是图片?而不是文案?
        • 保存图片涉及用户隐私,需适配6.0动态权限
        • 如何确定要保存的图片是Url?还是base64?
        • 保存图片
        • 刷新图库
      • 扩展函数
        • Bitmap 扩展函数
        • ContentResolver 扩展函数
        • Uri 扩展函数
        • String扩展函数(图片格式)
        • PictureSave 顶层文件(涵盖所用扩展函数)
      • 项目实战

功能分析

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

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

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

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

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

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

在这里插入图片描述

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

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

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

    setOnLongClickListener {val hitTestResult: HitTestResult = hitTestResult// 如果是图片类型或者是带有图片链接的类型if (hitTestResult.type == HitTestResult.IMAGE_TYPE ||hitTestResult.type == HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {val extra = hitTestResult.extraTimber.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 {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) ?: extraval 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) ?: extraval 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() / 1000put(MediaStore.Images.Media.DATE_ADDED, date)put(MediaStore.Images.Media.DATE_MODIFIED, date)}// 保存的位置val collection: Uriif (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.nameWithoutExtensionval fileExtension = imageFile.extensionvar queryUri = this.queryMediaImage28(imageFile.absolutePath)var suffix = 1while (queryUri != null) {val newName = fileNameWithoutExtension + "(${suffix++})." + fileExtensionimageFile = 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.contentResolverval 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() / 1000put(MediaStore.Images.Media.DATE_ADDED, date)put(MediaStore.Images.Media.DATE_MODIFIED, date)}// 保存的位置val collection: Uriif (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.nameWithoutExtensionval fileExtension = imageFile.extensionvar queryUri = this.queryMediaImage28(imageFile.absolutePath)var suffix = 1while (queryUri != null) {val newName = fileNameWithoutExtension + "(${suffix++})." + fileExtensionimageFile = 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 nullval imageFile = File(imagePath)if (imageFile.canRead() && imageFile.exists()) {Timber.e("query: path: $imagePath exists")// 文件已存在,返回一个file://xxx的urireturn 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.PNGfileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> Bitmap.CompressFormat.JPEGfileName.endsWith(".webp") -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)Bitmap.CompressFormat.WEBP_LOSSLESS else Bitmap.CompressFormat.WEBPelse -> 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 xxximport 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.OutputStreamprivate 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.contentResolverval 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.PNGfileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> Bitmap.CompressFormat.JPEGfileName.endsWith(".webp") -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)Bitmap.CompressFormat.WEBP_LOSSLESS else Bitmap.CompressFormat.WEBPelse -> 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() / 1000put(MediaStore.Images.Media.DATE_ADDED, date)put(MediaStore.Images.Media.DATE_MODIFIED, date)}// 保存的位置val collection: Uriif (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.nameWithoutExtensionval fileExtension = imageFile.extensionvar queryUri = this.queryMediaImage28(imageFile.absolutePath)var suffix = 1while (queryUri != null) {val newName = fileNameWithoutExtension + "(${suffix++})." + fileExtensionimageFile = 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 nullval imageFile = File(imagePath)if (imageFile.canRead() && imageFile.exists()) {Timber.e("query: path: $imagePath exists")// 文件已存在,返回一个file://xxx的urireturn 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()) returnactivity.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? = nullprivate 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) ?: extraval 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) }}
}

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

相关内容

热门资讯

老子简介 老子简介  简介,即简明扼要的介绍。是当事人全面而简洁地介绍情况的一种书面表达方式,它是应用写作学研...
写作叙述方法之分叙法 写作叙述方法之分叙法  分叙法是指叙述两件或两件以上的同一时间内不同地点发生的事情,也叫平叙法。以下...
匆匆仿写作文 匆匆仿写作文(精选30篇)  作文,就是将生活中的见闻、感受描绘出来,将对生活的想像与思考表达出来,...
中国文学常识 中国文学常识大全  常识,一般指从事各项工作以及进行学术研究所需具备的相关领域内的基础知识。下面是小...
佛寺庙宇对联 佛寺庙宇对联(精选70句)  寺庙对联内容丰富,言简意赅。是我国佛教文化重要组成部分,也是我国寺庙不...
中国四大名著文学常识 中国四大名著文学常识  中国四大名著是指《水浒传》《三国演义》《西游记》《红楼梦》(按照成书先后顺序...
圆明园祭阅读答案   (1)那天很冷,我却刻意要到圆明园去。朋友们都劝说,圆明园没有什么可看的,只是几块烂石头,我说,...
秋后的蚂蚱歇后语是什么   歇后语是我国人民在生活实践中创造的一种特殊语言形式。它一般由两个部分构成,前半截是形象的比喻, ...
教师节对联 教师节对联(精选55句)  在社会一步步向前发展的'今天,大家总少不了接触一些耳熟能详的对联吧,对联...
新年的对联 新年的对联(精选115句)  在现在的社会生活中,大家都经常接触到对联吧,对联作为一种习俗,是汉族传...
描写天气谚语 描写天气谚语大全  在平凡的学习、工作、生活中,大家都有令自己印象深刻的谚语吧,谚语是劳动人民的生活...
雷声大雨点小歇后语是什么   雷声大雨点小——(虚张声势):比喻做起事来声势造得很大,实际行动却很少。  雷声大,雨点小 : ...
小学优秀班主任事迹材料 小学优秀班主任事迹材料(精选11篇)  根据自己的兴趣爱好,成立活动小组,再聘请相关学科老师为指导教...
王羲之兰亭序全文及译文 王羲之兰亭序全文及译文  永和九年,岁在癸丑,暮春之初,会于会稽山阴之兰亭,修稧事也。下面是小编为你...
仰仗是褒义词吗 仰仗是褒义词吗  仰仗常指事物的根基状态,依靠;依赖,我们看看下面的相关资料吧!  仰仗是褒义词吗?...
自考英语写作基础试题及答案 自考英语写作基础试题及答案  从小学、初中、高中到大学乃至工作,我们都要用到试题,试题是命题者根据一...
你陪伴我长大的作文500字 你陪伴我长大的作文500字  在我六岁那年,一个小小的你——植物小马诞生了。你没有其他玩具那么美丽,...
战争与和平的作者是谁 战争与和平的作者是谁  《战争与和平》以极其简洁的文字,卓越的、令人惊叹的心理分析,生动、鲜活地描绘...
记叙文知识架构详解 记叙文知识架构详解  语文阅读在平时测试、期末考试中都占有很大的比重,而记叙文阅读是学生失分率最高的...
野性的呼唤简介及读书笔记 野性的呼唤简介及读书笔记  作者:杰克伦敦  改写:何碧珠  出版社:中国东方出版社  内容简介: ...