去年同事写了一个 “在H5中保存图片到相册” 的功能,虽然有大致实现思路,实现起来也没问题,但是感觉同事考虑问题的很周全,当时候就想着去学习一下,但是项目太赶没顾得上,索性现在有时间,准备好好学习一下
我那些关于WebView的回忆 ~ 包含入门使用、优化加载样式、监听加载状态、各场景后退键处理、俩端交互流程、header、user–agent传值、交互常见问题、较全API整合
业务场景:Android端使用WebView加载H5时,如果用户长按其内部图片,则弹框提示用户可保存图片
简单说一下我的实现思路:首先监听WebView长按事件 → 判断长按的内容是否为图片类型 → 判断图片类型是url、还是base64 → 如果是url就下载图片保存 → 如果是base64则转Bitmap进行保存 → 保存成功刷新相册图库
功能分析
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.extra
Timber.e("图片地址或base64:$extra")
}
setOnLongClickListener {
val hitTestResult: HitTestResult = hitTestResult
// 如果是图片类型或者是带有图片链接的类型
if (hitTestResult.type == HitTestResult.IMAGE_TYPE ||
hitTestResult.type == HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) {
val extra = hitTestResult.extra
Timber.e("图片地址或base64:$extra")
longClickListener?.invoke(extra)
}
true
}
保存图片涉及用户隐私,需适配6.0动态权限
关于 Android6.0适配 是很老的东西了,具体使用哪种方式可自行定义(同事使用的是Google原始权限请求方式)
Look
:当用户拒绝授权后,再次申请权限时需跳转应用设置内开启授权,关于这方面也可做兼容适配,具体适配方式记录于 Android兼容适配 – 不同机型跳转应用权限设置页面
private val permission by lazy { Manifest.permission.WRITE_EXTERNAL_STORAGE }
private val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) return@registerForActivityResult savePicture()
if (shouldShowRequestPermissionRationale(permission)) {
activity?.alertDialog {
setTitle("权限申请")
setMessage("我们需要获取写文件权限, 否则您将无法正常使用图片保存功能")
setNegativeButton("取消")
setPositiveButton("申请授权") { checkPermission() }
}
} else {
activity?.alertDialog {
setTitle("权限申请")
setMessage("由于无法获取读文件权限, 无法正常使用图片保存功能, 请开启权限后再使用。nn设置路径: 应用管理->华安基金->权限")
setNegativeButton("取消")
setPositiveButton("去设置") {
activity?.let { context -> PermissionPageUtils(context).jumpPermissionPage() }
}
}
}
}
private fun checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& ContextCompat.checkSelfPermission(AppContext, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) { // 无权限
return permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
savePicture()
}
下方为 Context
扩展出来的 Dialog函数
,无需太过关注,上方弹框可自定义样式(或用原始Dialog
);
项目中 Dialog
用到的addOnGlobalLayoutListener
监听
fun Context.alertDialog(builder: AppDialogBuilder.() -> Unit): AppDialogBuilder {
val alertDialogUi = AlertDialogUi(this)
alertDialogUi.viewTreeObserver.addOnGlobalLayoutListener {
if (alertDialogUi.height > AppContext.screenHeight / 3 * 2) {
alertDialogUi.updateLayoutParams<ViewGroup.LayoutParams> {
height = AppContext.screenHeight / 3 * 2 - dip(20)
}
}
}
val alertDialogBuilder = AlertDialog.Builder(this).setCancelable(false).setView(alertDialogUi)
val appDialogBuilder = AppDialogBuilder(alertDialogUi, alertDialogBuilder)
appDialogBuilder.builder()
appDialogBuilder.show()
return appDialogBuilder
}
如何确定要保存的图片是Url?还是base64?
在双端交互时涉及到图片展示、保存相关需求的话,一般会有俩种传递方式,一种为图片的url地址,一种为base64串;
去年年初
的时候有一个交互需求是H5调用拍照、相册功能,然后将所选照片传给H5,这里我使用的方式就是将图片转为了base64串,然后传给H5用于展示
,其中涉及到了一些相关知识,不了解的话,可以去学习一下 – Android进阶之路 – 双端交互之传递Base64图片
话说回头,继续往下看
因为在长按时我们已经判断肯定是图片类型了,接下来通过 URLUtil.isValidUrl(extra)
判断其有效性;由此区分是图片url还是base64,然后将其转为bitmap用于存储
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)
}
保存图片
我项目里用了协程
去切换线程
,具体可根据自身项目场景使用不同方式去实现;图片下载方式用的是Glide框架
,如果对 Glide 基础方面,了解不足的话,可以去我的Glide基础篇简单巩固下
关于 saveToAlbum
函数具体实现,会在下方的扩展函数中声明
private fun savePicture() {
lifecycleScope.launch(Dispatchers.IO) {
try {
withContext(Dispatchers.Main) { loadingState(LoadingState.LoadingStart) }
val bitmap = if (URLUtil.isValidUrl(extra)) {
activity?.let { Glide.with(it).asBitmap().load(extra).submit().get() }
} else {
val base64 = extra?.split(",")?.getOrNull(1) ?: extra
val decode = Base64.decode(base64, Base64.NO_WRAP)
BitmapFactory.decodeByteArray(decode, 0, decode.size)
}
Timber.d("保存相册图片大小:${bitmap?.byteCount}")
saveToAlbum(bitmap, "ha_${System.currentTimeMillis()}.png")
} catch (throwable: Throwable) {
Timber.e(throwable)
showToast("保存到系统相册失败")
} finally {
withContext(Dispatchers.Main) { loadingState(LoadingState.LoadingEnd) }
dismissAllowingStateLoss()
}
}
}
private suspend fun saveToAlbum(bitmap: Bitmap?, fileName: String) {
if (bitmap.isNull() || activity.isNull()) {
return showToast("保存到系统相册失败")
}
val pictureUri = activity?.let { bitmap.saveToAlbum(it, fileName) }
if (pictureUri == null) showToast("保存到系统相册失败") else showToast("已保存到系统相册")
}
刷新图库
其实同事考虑的问题也挺完善,内部也做了兼容(不可直接使用,需结合下方的扩展函数)
/**
* 插入图片到媒体库
*/
@Suppress("DEPRECATION")
private fun ContentResolver.insertMediaImage(fileName: String, outputFileTaker: OutputFileTaker? = null): Uri? {
// 图片信息
val imageValues = ContentValues().apply {
val mimeType = fileName.getMimeType()
if (mimeType != null) {
put(MediaStore.Images.Media.MIME_TYPE, mimeType)
}
val date = System.currentTimeMillis() / 1000
put(MediaStore.Images.Media.DATE_ADDED, date)
put(MediaStore.Images.Media.DATE_MODIFIED, date)
}
// 保存的位置
val collection: Uri
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
imageValues.apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
put(MediaStore.Images.Media.IS_PENDING, 1)
}
collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
// 高版本不用查重直接插入,会自动重命名
} else {
// 老版本
val pictures = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
if (!pictures.exists() && !pictures.mkdirs()) {
Timber.e("save: error: can't create Pictures directory")
return null
}
// 文件路径查重,重复的话在文件名后拼接数字
var imageFile = File(pictures, fileName)
val fileNameWithoutExtension = imageFile.nameWithoutExtension
val fileExtension = imageFile.extension
var queryUri = this.queryMediaImage28(imageFile.absolutePath)
var suffix = 1
while (queryUri != null) {
val newName = fileNameWithoutExtension + "(${suffix++})." + fileExtension
imageFile = File(pictures, newName)
queryUri = this.queryMediaImage28(imageFile.absolutePath)
}
imageValues.apply {
put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name)
Timber.e("save file: $imageFile.absolutePath") // 保存路径
put(MediaStore.Images.Media.DATA, imageFile.absolutePath)
}
outputFileTaker?.file = imageFile// 回传文件路径,用于设置文件大小
collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
// 插入图片信息
return this.insert(collection, imageValues)
}
扩展函数
创建一个顶层文件 PictureSave,放置图片相关的顶层函数,更加方便调用
Bitmap 扩展函数
/**
* 保存Bitmap到相册的Pictures文件夹
*
* 官网文档:https://developer.android.google.cn/training/data-storage/shared/media
*
* @param context 上下文
* @param fileName 文件名。 需要携带后缀
* @param quality 质量(图片质量决定了图片大小)
*/
internal fun Bitmap.saveToAlbum(context: Context, fileName: String, quality: Int = 75): Uri? {
// 插入图片信息
val resolver = context.contentResolver
val outputFile = OutputFileTaker()
val imageUri = resolver.insertMediaImage(fileName, outputFile)
if (imageUri == null) {
Timber.e("insert: error: uri == null")
return null
}
// 保存图片
(imageUri.outputStream(resolver) ?: return null).use {
val format = fileName.getBitmapFormat()
this@saveToAlbum.compress(format, quality, it)
imageUri.finishPending(context, resolver, outputFile.file)
}
return imageUri
}
private fun Uri.outputStream(resolver: ContentResolver): OutputStream? {
return try {
resolver.openOutputStream(this)
} catch (e: FileNotFoundException) {
Timber.e("save: open stream error: $e")
null
}
}
ContentResolver 扩展函数
/**
* 插入图片到媒体库
*/
@Suppress("DEPRECATION")
private fun ContentResolver.insertMediaImage(fileName: String, outputFileTaker: OutputFileTaker? = null): Uri? {
// 图片信息
val imageValues = ContentValues().apply {
val mimeType = fileName.getMimeType()
if (mimeType != null) {
put(MediaStore.Images.Media.MIME_TYPE, mimeType)
}
val date = System.currentTimeMillis() / 1000
put(MediaStore.Images.Media.DATE_ADDED, date)
put(MediaStore.Images.Media.DATE_MODIFIED, date)
}
// 保存的位置
val collection: Uri
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
imageValues.apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
put(MediaStore.Images.Media.IS_PENDING, 1)
}
collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
// 高版本不用查重直接插入,会自动重命名
} else {
// 老版本
val pictures = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
if (!pictures.exists() && !pictures.mkdirs()) {
Timber.e("save: error: can't create Pictures directory")
return null
}
// 文件路径查重,重复的话在文件名后拼接数字
var imageFile = File(pictures, fileName)
val fileNameWithoutExtension = imageFile.nameWithoutExtension
val fileExtension = imageFile.extension
var queryUri = this.queryMediaImage28(imageFile.absolutePath)
var suffix = 1
while (queryUri != null) {
val newName = fileNameWithoutExtension + "(${suffix++})." + fileExtension
imageFile = File(pictures, newName)
queryUri = this.queryMediaImage28(imageFile.absolutePath)
}
imageValues.apply {
put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name)
Timber.e("save file: $imageFile.absolutePath") // 保存路径
put(MediaStore.Images.Media.DATA, imageFile.absolutePath)
}
outputFileTaker?.file = imageFile// 回传文件路径,用于设置文件大小
collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
// 插入图片信息
return this.insert(collection, imageValues)
}
/**
* Android Q以下版本,查询媒体库中当前路径是否存在
* @return Uri 返回null时说明不存在,可以进行图片插入逻辑
*/
@Suppress("DEPRECATION")
private fun ContentResolver.queryMediaImage28(imagePath: String): Uri? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return null
val imageFile = File(imagePath)
if (imageFile.canRead() && imageFile.exists()) {
Timber.e("query: path: $imagePath exists")
// 文件已存在,返回一个file://xxx的uri
return Uri.fromFile(imageFile)
}
// 保存的位置
val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
// 查询是否已经存在相同图片
val query = this.query(
collection,
arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA),
"${MediaStore.Images.Media.DATA} == ?",
arrayOf(imagePath), null
)
query?.use {
while (it.moveToNext()) {
val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val id = it.getLong(idColumn)
return ContentUris.withAppendedId(collection, id)
}
}
return null
}
Uri 扩展函数
private fun Uri.outputStream(resolver: ContentResolver): OutputStream? {
return try {
resolver.openOutputStream(this)
} catch (e: FileNotFoundException) {
Timber.e("save: open stream error: $e")
null
}
}
@Suppress("DEPRECATION")
private fun Uri.finishPending(context: Context, resolver: ContentResolver, outputFile: File?) {
val imageValues = ContentValues()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (outputFile != null) {
imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length())
}
resolver.update(this, imageValues, null, null)
// 通知媒体库更新
val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this)
context.sendBroadcast(intent)
} else {
// Android Q添加了IS_PENDING状态,为0时其他应用才可见
imageValues.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(this, imageValues, null, null)
}
}
String扩展函数(图片格式)
@Suppress("DEPRECATION")
private fun String.getBitmapFormat(): Bitmap.CompressFormat {
val fileName = this.lowercase()
return when {
fileName.endsWith(".png") -> Bitmap.CompressFormat.PNG
fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> Bitmap.CompressFormat.JPEG
fileName.endsWith(".webp") -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
Bitmap.CompressFormat.WEBP_LOSSLESS else Bitmap.CompressFormat.WEBP
else -> Bitmap.CompressFormat.PNG
}
}
private fun String.getMimeType(): String? {
val fileName = this.lowercase()
return when {
fileName.endsWith(".png") -> "image/png"
fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> "image/jpeg"
fileName.endsWith(".webp") -> "image/webp"
fileName.endsWith(".gif") -> "image/gif"
else -> null
}
}
PictureSave 顶层文件(涵盖所用扩展函数)
package xxx
import android.content.*
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
import java.io.OutputStream
private class OutputFileTaker(var file: File? = null)
/**
* 保存Bitmap到相册的Pictures文件夹
*
* https://developer.android.google.cn/training/data-storage/shared/media
*
* @param context 上下文
* @param fileName 文件名。 需要携带后缀
* @param quality 质量
*/
internal fun Bitmap.saveToAlbum(context: Context, fileName: String, quality: Int = 75): Uri? {
// 插入图片信息
val resolver = context.contentResolver
val outputFile = OutputFileTaker()
val imageUri = resolver.insertMediaImage(fileName, outputFile)
if (imageUri == null) {
Timber.e("insert: error: uri == null")
return null
}
// 保存图片
(imageUri.outputStream(resolver) ?: return null).use {
val format = fileName.getBitmapFormat()
this@saveToAlbum.compress(format, quality, it)
imageUri.finishPending(context, resolver, outputFile.file)
}
return imageUri
}
private fun Uri.outputStream(resolver: ContentResolver): OutputStream? {
return try {
resolver.openOutputStream(this)
} catch (e: FileNotFoundException) {
Timber.e("save: open stream error: $e")
null
}
}
@Suppress("DEPRECATION")
private fun Uri.finishPending(context: Context, resolver: ContentResolver, outputFile: File?) {
val imageValues = ContentValues()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (outputFile != null) {
imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length())
}
resolver.update(this, imageValues, null, null)
// 通知媒体库更新
val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this)
context.sendBroadcast(intent)
} else {
// Android Q添加了IS_PENDING状态,为0时其他应用才可见
imageValues.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(this, imageValues, null, null)
}
}
@Suppress("DEPRECATION")
private fun String.getBitmapFormat(): Bitmap.CompressFormat {
val fileName = this.lowercase()
return when {
fileName.endsWith(".png") -> Bitmap.CompressFormat.PNG
fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> Bitmap.CompressFormat.JPEG
fileName.endsWith(".webp") -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
Bitmap.CompressFormat.WEBP_LOSSLESS else Bitmap.CompressFormat.WEBP
else -> Bitmap.CompressFormat.PNG
}
}
private fun String.getMimeType(): String? {
val fileName = this.lowercase()
return when {
fileName.endsWith(".png") -> "image/png"
fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> "image/jpeg"
fileName.endsWith(".webp") -> "image/webp"
fileName.endsWith(".gif") -> "image/gif"
else -> null
}
}
/**
* 插入图片到媒体库
*/
@Suppress("DEPRECATION")
private fun ContentResolver.insertMediaImage(fileName: String, outputFileTaker: OutputFileTaker? = null): Uri? {
// 图片信息
val imageValues = ContentValues().apply {
val mimeType = fileName.getMimeType()
if (mimeType != null) {
put(MediaStore.Images.Media.MIME_TYPE, mimeType)
}
val date = System.currentTimeMillis() / 1000
put(MediaStore.Images.Media.DATE_ADDED, date)
put(MediaStore.Images.Media.DATE_MODIFIED, date)
}
// 保存的位置
val collection: Uri
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
imageValues.apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
put(MediaStore.Images.Media.IS_PENDING, 1)
}
collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
// 高版本不用查重直接插入,会自动重命名
} else {
// 老版本
val pictures = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
if (!pictures.exists() && !pictures.mkdirs()) {
Timber.e("save: error: can't create Pictures directory")
return null
}
// 文件路径查重,重复的话在文件名后拼接数字
var imageFile = File(pictures, fileName)
val fileNameWithoutExtension = imageFile.nameWithoutExtension
val fileExtension = imageFile.extension
var queryUri = this.queryMediaImage28(imageFile.absolutePath)
var suffix = 1
while (queryUri != null) {
val newName = fileNameWithoutExtension + "(${suffix++})." + fileExtension
imageFile = File(pictures, newName)
queryUri = this.queryMediaImage28(imageFile.absolutePath)
}
imageValues.apply {
put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name)
Timber.e("save file: $imageFile.absolutePath") // 保存路径
put(MediaStore.Images.Media.DATA, imageFile.absolutePath)
}
outputFileTaker?.file = imageFile// 回传文件路径,用于设置文件大小
collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
// 插入图片信息
return this.insert(collection, imageValues)
}
/**
* Android Q以下版本,查询媒体库中当前路径是否存在
* @return Uri 返回null时说明不存在,可以进行图片插入逻辑
*/
@Suppress("DEPRECATION")
private fun ContentResolver.queryMediaImage28(imagePath: String): Uri? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return null
val imageFile = File(imagePath)
if (imageFile.canRead() && imageFile.exists()) {
Timber.e("query: path: $imagePath exists")
// 文件已存在,返回一个file://xxx的uri
return Uri.fromFile(imageFile)
}
// 保存的位置
val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
// 查询是否已经存在相同图片
val query = this.query(
collection,
arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA),
"${MediaStore.Images.Media.DATA} == ?",
arrayOf(imagePath), null
)
query?.use {
while (it.moveToNext()) {
val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val id = it.getLong(idColumn)
return ContentUris.withAppendedId(collection, id)
}
}
return null
}
项目实战
Activity
webView.setLongClickListener {
ComponentService.service?.savePicture(this, it)// 弹出保存图片的对话框
}
Fragment
webView.setLongClickListener {
activity?.run { ComponentService.service?.savePicture(this, it) }// 弹出保存图片的对话框
}
原项目中使用了接口包装,我们只看 savePicture
具体实现
override fun savePicture(activity: FragmentActivity, extra: String?) {
if (extra.isNullOrEmpty()) return
activity.currentFocus?.clearFocus()
activity.showAsync({ PictureSaveBottomSheetDialogFragment() }, tag = "PictureSaveBottomSheetDialogFragment") {
this.extra = extra
}
}
因为项目用的MVI框架,可自行忽略部分实现,主要关注自己想看的...
PictureSaveBottomSheetDialogFragment
internal class PictureSaveBottomSheetDialogFragment : BaseMavericksBottomSheetDialogFragment() {
private val permission by lazy { Manifest.permission.WRITE_EXTERNAL_STORAGE }
var extra: String? = null
private val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) return@registerForActivityResult savePicture()
if (shouldShowRequestPermissionRationale(permission)) {
activity?.alertDialog {
setTitle("权限申请")
setMessage("我们需要获取写文件权限, 否则您将无法正常使用图片保存功能")
setNegativeButton("取消")
setPositiveButton("申请授权") { checkPermission() }
}
} else {
activity?.alertDialog {
setTitle("权限申请")
setMessage("由于无法获取读文件权限, 无法正常使用图片保存功能, 请开启权限后再使用。nn设置路径: 应用管理->华安基金->权限")
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) }
}
}
全都过一次后,也是收获满满,争取明天再进一步,加油 > < ~
原文地址:https://blog.csdn.net/qq_20451879/article/details/128152152
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_10103.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!