去年同事写了一个 “在H5中保存图片到相册” 的功能,虽然有大致实现思路,实现起来也没问题,但是感觉同事考虑问题的很周全,当时候就想着去学习一下,但是项目太赶没顾得上,索性现在有时间,准备好好学习一下
业务场景:Android端使用WebView加载H5时,如果用户长按其内部图片,则弹框提示用户可保存图片
简单说一下我的实现思路:首先监听WebView长按事件 → 判断长按的内容是否为图片类型 → 判断图片类型是url、还是base64 → 如果是url就下载图片保存 → 如果是base64则转Bitmap进行保存 → 保存成功刷新相册图库
Here:根据业务场景,来拆分一下具体实现中需要考虑的事情
首先在 WebView
支持通过setOnLongClickListener
监听长按事件
override fun setOnLongClickListener(l: OnLongClickListener?) {super.setOnLongClickListener(l)}
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}
关于 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串;
去年年初
的时候有一个交互需求是H5调用拍照、相册功能,然后将所选照片传给H5,这里我使用的方式就是将图片转为了base64串,然后传给H5用于展示
,其中涉及到了一些相关知识,不了解的话,可以去学习一下 - Android进阶之路 - 双端交互之传递Base64图片
话说回头,继续往下看
因为在长按时我们已经判断肯定是图片类型了,接下来通过 URLUtil.isValidUrl(extra)
判断其有效性;由此区分是图片url还是base64,然后将其转为bitmap用于存储
URLUtil
是 Google
提供的原始类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到相册的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 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
}
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}
}
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) }}
}
全都过一次后,也是收获满满,争取明天再进一步,加油 > < ~
上一篇:字符函数和字符串函数
下一篇:PHP Libxml 函数