去年同事寫了一個 “在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
類,方便獲取用戶操作時的類型結果
可以通過類型判斷,得知用戶是否在操作圖片
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用于存儲
-
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) ?: extra
val decode = Base64.decode(base64, Base64.NO_WRAP)
BitmapFactory.decodeByteArray(decode, 0, decode.size)
}
URLUtil.isValidUrl()
內部實現(xiàn)
保存圖片
我項目里用了協(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文章來源:http://www.zghlxwxcb.cn/news/detail-408875.html
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模板網!