Android WebView相关交互实现

一、支持网页视频播放

设置自定义WebChromeClient,覆写 onShowCustomView onHideCustomView,将WebChromeClient提供的 view 添加到布局中即可。

webChromeClient = object : WebChromeClient() {
    override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
        requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
        mViewBinding.flVideoContainer.run {
            visibility = View.VISIBLE
            addView(view)
        }
    }

    override fun onHideCustomView() {
        requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
        mViewBinding.flVideoContainer.run {
            visibility = View.GONE
            removeAllViews()
        }
    }
}

二、长按保存图片

首先我们为 WebView 设置长按点击事件监听,通过调用 WebView#getHitTestResult() 获取当前点击类型。

setOnLongClickListener {
    val hitResult = hitTestResult
    when (hitResult.type) {
        WebView.HitTestResult.IMAGE_TYPE,
        WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE -> {
            saveToGallery(hitResult.extra)
            true
        }
        else -> false
    }
}

接下来弹窗展示选项,这里使用了 AlertDialog。

private fun saveToGallery(url: String?) {
    AlertDialog.Builder(this@MainActivity)
        .setItems(arrayOf("保存到相册")) { dialog, _ ->
            dialog.dismiss()
            url.run str@{
                if (isNullOrBlank()) {
                    Toast.makeText(
                        this@MainActivity,
                        "保存失败, 图片路径为空",
                        Toast.LENGTH_LONG
                    ).show()
                    return@str
                }
                val uri = toUri()
                when (uri.scheme) {
                    "http", "https" -> saveUrlImage(this)
                    "data" -> saveBase64Image(uri.schemeSpecificPart)
                    else -> Toast.makeText(
                        this@MainActivity,
                        "保存失败, 不支持的格式",
                        Toast.LENGTH_LONG
                    ).show()
                }
            }
        }
        .create()
        .show()
}

图片 Uri 分为 http、data 格式进行加载。

Uri Data Scheme 格式如下:

  data:[<mime type>][;charset=<charset>][;<encoding>],<encoded data>

  ①.  data :协议名称;

  ②.  [<mime type>] :可选项,数据类型(image/png、text/plain等)

  ③.  [;charset=<charset>] :可选项,源文本的字符集编码方式

  ④.  [;<encoding>] :数据编码方式(默认US-ASCII,BASE64两种)

  ⑤.  ,<encoded data> :编码后的数据

例如 data:image/jpeg;base64,aGFoYQ==

这里使用 URLConnection 获取网络资源,Kotlin协程进行异步操作。

private fun saveUrlImage(url: String) = launch(Dispatchers.IO) {
    try {
        URL(url).openStream().use {
            val fileName = url.substringAfterLast('/')
            val extension = fileName.substringAfterLast('.')
            insertImageToMediaStore(
                "image_${fileName}",
                "image/${if (extension.isEmpty()) "jpeg" else extension}",
                it.readBytes()
            )
        }
    } catch (e: IOException) {
        e.printStackTrace()
        showToast("保存失败, 网络连接错误")
    }
}

private fun saveBase64Image(resource: String) = launch(Dispatchers.IO) {
    val mimeType = resource.substringBefore(';', "")
    val type = mimeType.split('/')
    if (type.size != 2) {
        showToast("保存失败")
        return@launch
    }
    if (type[0] != "image") {
        showToast("保存失败, 不支持的类型")
        return@launch
    }
    val base64 = resource.substringAfter("base64,", "")
    if (base64.isEmpty()) {
        showToast("保存失败, 不支持的数据格式")
        return@launch
    }
    val bytes = Base64.decode(base64, Base64.DEFAULT)
    val date = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date())
    insertImageToMediaStore("image_$date.${type[1]}", mimeType, bytes)
}

最后通过 ContentResolver 保存上一步获取的字节数组即可。

private suspend fun insertImageToMediaStore(title: String, mimeType: String, bytes: ByteArray) =
    withContext(Dispatchers.IO) {
        val insertValues = ContentValues().apply {
            val now = System.currentTimeMillis() / 1000
            put(MediaColumns.DISPLAY_NAME, title)
            put(MediaColumns.MIME_TYPE, mimeType)
            put(MediaColumns.DATE_ADDED, now)
            put(MediaColumns.DATE_MODIFIED, now)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaColumns.IS_PENDING, 1)
                put(
                    MediaColumns.DATE_EXPIRES,
                    (System.currentTimeMillis() + DateUtils.DAY_IN_MILLIS) / 1000
                )
            }
        }
        val contentResolver = contentResolver
        val pendingUri: Uri? = contentResolver.insert(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            insertValues
        )
        if (pendingUri == null) {
            showToast("保存失败")
            return@withContext
        }
        try {
            contentResolver.openOutputStream(pendingUri).use {
                if (it == null) {
                    showToast("保存失败")
                    return@use
                }
                it.write(bytes)
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                insertValues.put(MediaColumns.IS_PENDING, 0)
                insertValues.putNull(MediaColumns.DATE_EXPIRES)
                contentResolver.update(pendingUri, insertValues, null, null)
            }
            showToast("保存成功")
        } catch (e: Exception) {
            contentResolver.delete(pendingUri, null, null)
            showToast("保存失败")
        }
    }

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注