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
- Android interacts with front-end H5 through WebView
- Reasons and solutions for possible errors in the interaction between Andorid and H5 (JS)
Business combat
- Scenario: Two-terminal interactive transmission of pictures (Base64)
- Scenario: Save pictures in WebView to album
业务场景:Android端使用WebView加载H5时,如果用户长按其内部图片,则弹框提示用户可保存图片
简单说一下我的实现思路:首先监听WebView长按事件 → 判断长按的内容是否为图片类型 → 判断图片类型是url、还是base64 → 如果是url就下载图片保存 → 如果是base64则转Bitmap进行保存 → 保存成功刷新相册图库
Functional Analysis
Here:根据业务场景,来拆分一下具体实现中需要考虑的事情
Does H5 support long press event monitoring?
First of all, WebView
support by setOnLongClickListener
listening 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?
WebView
HitTestResult
The class is provided to facilitate obtaining the type results of user operations
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 Context
extended 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 Dialog
used 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用于存储
URLUtil
is the original classGoogle
provided byextra
It 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
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 saveToAlbum
of 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 savePicture
the 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) }
}
}
全都过一次后,也是收获满满,争取明天再进一步,加油 > < ~