WebView 是一个基于 WebKit 引擎,展现 Web 页面的控件,Android 的 WebView 在低版本和高版本采用了不同的 WebKit 版本内核。
简单使用
WebView 最简单的使用方式是直接显示网页内容
<!-- 添加网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
webview.loadUrl(url)
常用方法
后退网页
//判断是否可以后退
webview.canGoBack()
//后退网页
webview.goBack()
销毁 Webview,先从父容器移除,再销毁。
father.removeView(webview)
webview.destroy()
在 Activity 中处理 Back 键事件
// 监听手机屏幕上的按键
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if ((keyCode == KEYCODE_BACK) && webview.canGoBack()) {
webview.goBack()
return true
}
return super.onKeyDown(keyCode, event)
}
清除缓存
//由于内核缓存是全局的,因此这个方法不仅针对该 Webview 而是针对整个应用程序。
webview.clearCache(true)
//清除当前 Webview 访问的历史记录
webview.clearHistory()
//清除自动完成填充的表单数据
webview.clearFormData()
WebSettings
作用:对 WebView 进行配置和管理
下面是一些常规设置
val webSettings = webview.settings
with(webSettings) {
javaScriptEnabled = true //支持 JS
javaScriptCanOpenWindowsAutomatically = true //支持通过 JS 打开新窗口
domStorageEnabled = true //支持 DOM Storage
defaultTextEncodingName = "utf-8" //设置编码格式
loadsImagesAutomatically = true //支持自动加载图片
setSupportZoom(true) //支持缩放,默认为 true。是下面属性的前提
builtInZoomControls = true //设置内置的缩放控件,若为 false,则该 WebView 不可缩放
displayZoomControls = true //隐藏原生的缩放控件
databaseEnabled = true //数据库存储 API 是否可用
cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK //设置缓存,只要本地有就使用缓存中的数据,本地没有才从网络上获取
allowFileAccess = true //设置可以访问文件
//下面两者合用,可设置自适应屏幕
useWideViewPort = true //将图片调整到适合 WebView 的大小
loadWithOverviewMode = true //缩放至屏幕大小
}
WebViewClient
在影响 View 的事件到来时,会通过 WebViewClient 中的方法回调。
webview.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
view.loadUrl(url)
return true
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
//开始载入页面时调用此方法,在这里我们可以设定一个 loading 的页面
}
override fun onPageFinished(view: WebView?, url: String?) {
//在页面加载结束时调用,在这里我们可以关闭 loading
}
override fun onLoadResource(view: WebView?, url: String?) {
//在加载页面资源时会调用,比如图片
}
}
WebChromeClient
当影响浏览器的事件到来时,会通过 WebChromeClient 中的方法回调。
webview.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
//获得网页的加载进度并显示
progressbar.text = if (newProgress < 100) "${newProgress}%" else "100%"
}
override fun onReceivedTitle(view: WebView?, title: String?) {
//获取Web页面中的标题
title_text.text = title
}
}
WebView 与 JS 的交互
Android 通过 WebView 调用 JS 代码
通过 WebView 的 loadUrl
// 先载入 JS 代码
webview.loadUrl(url)
// 调用 javascript 的 callJS() 方法,调用的 JS 的方法名要对应上。
webview.loadUrl("javascript:callJS()")
也可以通过 WebView 的 evaluateJavascript,这个比上面的方法效率更高,因为该方法的执行不会使页面刷新,而 loadUrl 会,该方法在 Android 4.4 后才可使用,不过现在的安卓版本基本上都高于 4.4 了,所以这点不用在意了。如果需要返回值的话,请务必使用这个方法。
webview.evaluateJavascript("javascript:callJS()", object : ValueCallback<String> {
override fun onReceiveValue(p0: String?) {
//此处为 js 返回的结果
}
})
JS 通过 WebView 调用 Android 代码
通过 WebView 的 addJavascriptInterface 进行对象映射
//定义一个内部类
inner class AndroidJSInterFace{
// 定义 JS 需要调用的方法,被 JS 调用的方法要加上 @JavascriptInterface 注解
@JavascriptInterface
fun hello(msg: String?) {
runOnUiThread{
title_text.text = msg
}
}
}
//AndroidJSInterFace 类对象映射到 js 的 test 对象
webview.addJavascriptInterface(AndroidJSInterFace(), "test")
调起系统相机和相册
private var uploadMessageAboveL: ValueCallback<Array<Uri>>? = null
private var cameraFielPath: String? = null
private var uploadMessage: ValueCallback<Uri>? = null
webview.webChromeClient = object :WebChromeClient(){
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?
): Boolean {
uploadMessageAboveL = filePathCallback
openImageChooserActivity()
return true
}
// For Android < 3.0
fun openFileChooser(valueCallback: ValueCallback<Uri>) {
uploadMessage = valueCallback
openImageChooserActivity()
}
}
// 回调方法触发本地选择文件
private fun openImageChooserActivity() {
//拍照
val imageStorageDir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"heart_image"
)
if (!imageStorageDir.exists()) {
imageStorageDir.mkdirs()
}
cameraFielPath = imageStorageDir.toString() + File.separator + "IMG_" + System.currentTimeMillis()
.toString() + ".jpg"
val file = File(cameraFielPath)
//需要显示应用的意图列表,这个 list 的顺序和选择菜单上的图标顺序是相关的
val cameraIntents: MutableList<Intent> = ArrayList()
val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
val packageManager: PackageManager = packageManager
//获取手机里所有注册相机接收意图的应用程序,放到意图列表里
val listCam: List<ResolveInfo> = packageManager.queryIntentActivities(captureIntent, 0)
for (res in listCam) {
val packageName: String = res.activityInfo.packageName
val i = Intent(captureIntent)
i.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name)
i.setPackage(packageName)
i.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file))
cameraIntents.add(i)
}
//相册
val i = Intent(Intent.ACTION_GET_CONTENT)
i.action = Intent.ACTION_PICK
i.type = "image/*"
val chooserIntent = Intent.createChooser(i, "选择")
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toTypedArray())
startActivityForResult(
chooserIntent,
101
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 101) {
val result = if (data == null || resultCode != Activity.RESULT_OK) null else data.data
if (uploadMessageAboveL != null) {
onActivityResultAboveL(requestCode, resultCode, data)
} else if (uploadMessage != null) {
uploadMessage?.onReceiveValue(result)
uploadMessage = null
}
if (resultCode != RESULT_OK) {
//这里 uploadMessage 和 uploadMessageAboveL 在不同系统版本下分别持有了
//WebView 对象,在用户取消文件选择器的情况下,需给 onReceiveValue 传 null 返回值
//否则 WebView 在未收到返回值的情况下,无法进行任何操作,文件选择器会失效
if (uploadMessage != null) {
uploadMessage?.onReceiveValue(null)
uploadMessage = null
} else if (uploadMessageAboveL != null) {
uploadMessageAboveL?.onReceiveValue(null)
uploadMessageAboveL = null
}
}
}
}
// 选择内容回调到 Html 页面
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
fun onActivityResultAboveL(requestCode: Int, resultCode: Int, intent: Intent?) {
if (requestCode != 101 || uploadMessageAboveL == null)
return
var results = arrayOf<Uri>()
var result: Uri? = null
if (resultCode == Activity.RESULT_OK) {
if (intent != null) {
var dataString = intent.dataString
var clipData = intent.clipData
if (clipData != null) {
for (i in 0 until clipData.itemCount) {
var itemAt = clipData.getItemAt(i)
results[i] = itemAt.uri
}
}
if (dataString != null) {
results = arrayOf<Uri>(Uri.parse(dataString))
}
uploadMessageAboveL?.onReceiveValue(results)
uploadMessageAboveL = null
} else {
if (result == null && File(cameraFielPath).exists()) {
result = Uri.fromFile(File(cameraFielPath))
}
uploadMessageAboveL?.onReceiveValue(arrayOf(result!!))
uploadMessageAboveL = null
}
}
}
响应下载事件
webview.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
val uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
}
问题与优化
WebView 资源加载速度优化,主要针对图片。
Html 下载到 WebView 后,webkit 开始解析网页各个节点,发现有外部样式文件或外部脚本文件时,会异步发起网络请求下载文件,但如果在这之前也有解析到 image 节点,那势必也会发起网络请求下载相应的图片。在网络较差的情况下,过多的网络请求就会造成带宽紧张,影响到 css 或 js 文件加载完成的时间,造成页面空白过久。
优化建议: 告诉 WebView 先不要自动加载图片,等页面 finish 后再发起图片加载
webview.settings.loadsImagesAutomatically = Build.VERSION.SDK_INT < KITKAT
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {//页面完成之后再加载图片
super.onPageFinished(view, url)
if (!webview.settings.loadsImagesAutomatically) {
webview.settings.loadsImagesAutomatically = true
}
}
}
自定义加载异常的状态页面
当 WebView 加载页面出错时,比如 404 NOT FOUND,WebView 会默认显示一个出错界面,这个页面我们可以自定义比较好看的界面。
webview.webViewClient = object : WebViewClient() {
override fun onReceivedHttpError(
view: WebView?,
request: WebResourceRequest?,
errorResponse: WebResourceResponse?
) {
super.onReceivedHttpError(view, request, errorResponse)
if (errorResponse!!.statusCode == 404) {
webview.loadUrl(errUrl)
}
}
}
WebView 加载网页不显示图片问题
WebView从 Android 5 开始默认不允许混合模式,,https 当中不能加载 http 资源,但是在开发的时候可能使用的是 https 的链接,链接中的图片又是 http 的,导致显示图片失败,这时就需要设置混合模式了。
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
webview.settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
}
webview.settings.blockNetworkImage = false
}
WebView 无法唤起其他 app 问题
需要通过 url 链接唤起其他 App 的话,可以使用以下方式
private fun isInstall(intent: Intent): Boolean {
return App.getInstance().packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size > 0
}
private fun openApp(url: String): Boolean {
if (TextUtils.isEmpty(url)) return false
try {
if (!url.startsWith("http") || !url.startsWith("https") || !url.startsWith("ftp")) {
val uri = Uri.parse(url)
val host = uri.host
val scheme = uri.scheme
if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(scheme)) {
val intent = Intent(Intent.ACTION_VIEW, uri)
if (isInstall(intent)) {
startActivity(intent)
return true
}
}
}
} catch (e: Exception) {
return false
}
return false
}
最后在 shouldOverrideUrlLoading 中调用 openApp 方法即可