Web View
如果您希望在客户端应用中提供 Web 应用 (或只是网页) , 则可以使用 WebView 执行该操作.WebView 类是 Android 的 View 类的扩展, 可让您将网页显示为 Activity 布局的一部分.它不会包含功能全面的网络浏览器的任何功能, 例如导航控件或地址栏.WebView 默认只显示网页.
使用 WebView 非常有用的一种常见情形是, 您希望在应用中提供可能需要更新的信息, 例如最终用户协议或用户指南.在 Android 应用中, 您可以创建一个包含 WebView 的 Activity, 然后使用它来显示在线托管的文档.
另一种 WebView 可能会有所帮助的情形是, 如果您的应用向用户提供始终需要互联网连接才能检索数据的数据 (例如电子邮件) 在这种情况下, 您可能会发现相比于执行网络请求, 然后解析数据并在 Android 布局中呈现数据, 在 Android 应用中编译 WebView 以显示包含所有用户数据的网页更加轻松.您可以改为设计一个专为 Android 设备定制的网页, 然后在加载该网页的 Android 应用中实现 WebView.
管理 WebView 对象 - https://developer.android.google.cn/guide/webapps/managing-webview?hl=zh_cn 基于网络的内容 - https://developer.android.google.cn/guide/webapps?hl=zh_cn
实例
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Ffmpegmake"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/title_activity_main"
android:theme="@style/Theme.Ffmpegmake">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>webview 访问网络的白名单
android:networkSecurityConfig="@xml/network_security_config"
network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">tianditu.gov.cn</domain>
<domain includeSubdomains="true">192.168.20.130</domain>
</domain-config>
</network-security-config>activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".MainActivity">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>MainActivity
package org.yang.webrtc.ffmpeg.make
import android.os.Bundle
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main);
val webView = findViewById<WebView>(R.id.web_view)
WebView.setWebContentsDebuggingEnabled(true)
webView.settings.apply {
setSupportZoom(false)
displayZoomControls = false
builtInZoomControls = false
useWideViewPort = false
javaScriptEnabled = true
loadWithOverviewMode = true
defaultTextEncodingName = "UTF-8"
domStorageEnabled = true
allowContentAccess = true
}
webView.loadUrl("http://127.0.0.1/android/index_webrtc_test.html")
}
}加载本地 html
android_asset 方式
将 html 资源文件 放入\src\main\assets\sample
//web 视图 初始化
WebView web_view = (WebView)findViewById(R.id.web_view);
WebSettings webSettings = web_view.getSettings();
//如果访问的页面中要与Javascript交互, 则webview必须设置支持Javascript
webSettings.setJavaScriptEnabled(true);
// 若加载的 html 里有JS 在执行动画等操作, 会造成资源浪费 (CPU, 电量)
// 在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 即可
//支持插件
webSettings.setPluginsEnabled(true);
//设置自适应屏幕, 两者合用
webSettings.setUseWideViewPort(true); //将图片调整到适合webview的大小
webSettings.setLoadWithOverviewMode(true); // 缩放至屏幕的大小
//缩放操作
webSettings.setSupportZoom(true); //支持缩放, 默认为true; 是下面那个的前提;
webSettings.setBuiltInZoomControls(true); //设置内置的缩放控件; 若为false, 则该WebView不可缩放
webSettings.setDisplayZoomControls(false); //隐藏原生的缩放控件
//其他细节操作
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); //关闭webview中缓存
webSettings.setAllowFileAccess(true); //设置可以访问文件, 即本地HTML assets
webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持通过JS打开新窗口
webSettings.setLoadsImagesAutomatically(true); //支持自动加载图片
webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式
web_view.loadUrl("file:///android_asset/sample/sample.html");WebViewAssetLoader 安全域名方式
private fun createWebView(onPageFinished: Consumer<WebView>) {
val webView = findViewById<WebView>(R.id.webView)
WebView.setWebContentsDebuggingEnabled(true)
webView.settings.apply {
setSupportZoom(false)
displayZoomControls = false
builtInZoomControls = false
useWideViewPort = false
javaScriptEnabled = true
loadWithOverviewMode = true
defaultTextEncodingName = "UTF-8"
domStorageEnabled = true
allowContentAccess = true
}
// 使用 WebViewAssetLoader 安全加载
val assetLoader = WebViewAssetLoader.Builder()
.setDomain("xxxx.cn")
.addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(context))
.build()
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return assetLoader.shouldInterceptRequest(request.url)
}
override fun onPageFinished(view: WebView, url: String) { //这里不行 会和 onResume 重复通知
if (firstLoad) {
onPageFinished.accept(webView)
firstLoad = false; // 标记第一次加载已完成
}
}
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?) {
XLog.e("web onReceivedSslError ", error)
super.onReceivedSslError(view, handler, error)
}
override fun onReceivedError(
view: WebView?,
errorCode: Int,
description: String,
failingUrl: String?) {
XLog.e("web onReceivedError %s, %s", failingUrl, description)
}
}
interactiveMediation = Optional.of(
WebInteractiveFactory.create(webView, this)
)
//正式
webView.loadUrl("https://xxxx.cn/assets/html/dist-product/index.html")
}WebView 的状态
//激活WebView为活跃状态, 能正常执行网页的响应
webView.onResume() ;
//当页面被失去焦点被切换到后台不可见状态, 需要执行onPause
//通过onPause动作通知内核暂停所有的动作, 比如DOM的解析, plugin的执行, JavaScript执行;
webView.onPause();
//当应用程序(存在webview)被切换到后台时, 这个方法不仅仅针对当前的webview而是全局的全应用程序的webview
//它会暂停所有webview的layout, parsing, javascripttimer; 降低CPU功耗;
webView.pauseTimers()
//恢复pauseTimers状态
webView.resumeTimers();
//销毁Webview
//在关闭了Activity时, 如果Webview的音乐或视频, 还在播放; 就必须销毁Webview
//但是注意: webview调用destory时,webview仍绑定在Activity上
//这是由于自定义webview构建时传入了该Activity的context对象
//因此需要先从父容器中移除webview,然后再销毁webview:
rootLayout.removeView(webView);
webView.destroy();
Java & Javascript交互
/**
* Created by yangfh on 2019/6/20.
*/
public class ContainerJsOption {
private final WebView web;
public ContainerJsOption(WebView web) {
this.web = web;
}
public void removeTakeListView(SampleBean bean){
web.evaluateJavascript("javascript:removeTakeListView("+bean.getId()+")", (String value) ->{
});
}
public void removeReturnListView(SampleBean bean){
web.evaluateJavascript("javascript:removeReturnListView("+bean.getId()+")", (String value) ->{
});
}
public void removeViewByID(SampleBean bean){
web.evaluateJavascript("javascript:removeViewByID("+bean.getId()+")", (String value) ->{
});
}
}
/**
@android.webkit.JavascriptInterface 注解表明该方法, 可以被js调用
*/
public class ContainerMediator {
@JavascriptInterface
public String getLocalReturnList(){
Collection<SampleBean> beans = this.container.getCopyOfLocalReturnList();
return JacksonUtil.beanToJson(beans);
}
@JavascriptInterface
public String getLocalTakeList() {
Collection<SampleBean> beans = this.container.getCopyOfLocalTakeList();
return JacksonUtil.beanToJson(beans);
}
}注入 Java 对象
加载时, 注入一个全局 Container js 对象, 与java交互
WebView web_view = (WebView)findViewById(R.id.web_view);
ContainerJsOption jsopt = new ContainerJsOption(web_view);
containerMediator = new ContainerMediator(Container.getInstance(), jsopt);
web_view.addJavascriptInterface(containerMediator,"Container");
web_view.loadUrl("file:///android_asset/sample/list.html");调用 Javascript 函数
WebView web_view = (WebView)findViewById(R.id.web_view);
web_view.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果, 且只能传递字符串
}
});二进制数据分段
在传输文件时, 比如: 视频, 前端 js 占用内存太多, 会导致页面卡顿, 可以通过分段的方式读取
/**
* 由js分段读取, 防止内存太大
**/
WIM._storageBinaryReader = function(topic){
function append(allBinary, off, arr){
for (let index = off; index < allBinary.length; index++) {
allBinary[index] = arr.charCodeAt(index-off);
}
}
/**
* @param off 文件总偏移量
* @param capacity 缓冲大小
*/
var frist = WIM.storageBinaryReaderFragment(topic, 0, 0);
// console.log("WIM_WEB_518, storageBinaryReaderFragment frist = ", frist);
if (!frist) { return }
var fj = JSON.parse(frist)
/** meta 结构
{
total: 文件总大小
size: 本次读取的大小
data: 数据 base64编码
}*/
const total = fj.total
const capacity = 1024 * 1024 * 2
let off = 0
const allBinary = new Uint8Array(total);
while(off < total){
var meta =WIM.storageBinaryReaderFragment(topic, off, capacity);
var metaJson = JSON.parse(meta)
// console.log("WIM_WEB_518, storageBinaryReaderFragment metaJson = ", metaJson);
var binaryData = atob(metaJson.data);
append(allBinary, off, binaryData)
off += metaJson.size
}
var blob = new Blob([allBinary], { type: 'application/octet-stream' });
return blob;
}
WIM._storageBinaryWriter = function(topic, content){
// console.log("WIM_WEB_518, _storageBinaryWriter topic = ", topic, content.length);
WIM.storageBinaryWriter(topic, content)
}
@JavascriptInterface
public synchronized String storageBinaryReaderFragment(String topic, long off, int capacity) {
File storageFile = getStorageFile(topic);
if (!storageFile.exists()) {
return null;
}
try {
RandomAccessFile ras = new RandomAccessFile(storageFile, "r");
ras.seek(off);
byte[] binaryArray = new byte[capacity];
int size = ras.read(binaryArray);
String encodeToString = Base64.encodeToString(binaryArray, 0, size, Base64.DEFAULT);
JSONObject result = new JSONObject();
result.put("total", storageFile.length());
result.put("size", size);
result.put("data", encodeToString);
return result.toString();
} catch (Exception e) {
XLog.e(TAG+ "Exception storageBinaryReaderFragment: ", e);
}
return null;
}
@JavascriptInterface
public synchronized boolean storageBinaryWriter(String topic, String content) {
File storageFile = getStorageFile(topic);
try {
FileUtil.writeBytes(content.getBytes(Charsets.UTF_8), storageFile);
return true;
} catch (Exception e) {
XLog.e(TAG+ "Exception storageBinaryWriter: ", e);
}
return false;
}重定向 Console
提供实现 onConsoleMessage() 方法的 WebChromeClient,以便控制台消息显示在 Logcat 中。然后,使用 setWebChromeClient() 将 WebChromeClient 应用于 WebView。如需了解详情,请参阅 Webview 文档。
val myWebView: WebView = findViewById(R.id.webview)
myWebView.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(message: ConsoleMessage): Boolean {
Log.d("MyApplication", "${message.message()} -- From line " +
"${message.lineNumber()} of ${message.sourceId()}")
return true
}
}远程调试
https://developer.chrome.com/docs/devtools/remote-debugging/?hl=zh-cn https://developer.chrome.com/docs/devtools/remote-debugging/webviews?hl=zh-cn
1.配置 WebView 以进行调试
必须在您的应用中启用 WebView 调试。如需启用 WebView 调试,请对 WebView 类调用静态方法 setWebContentsDebuggingEnabled。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}此设置适用于应用的所有 WebView。
若一些特殊情况, 使用的是XWalkView, 可使用如下命令
XWalkPreferences.setValue(XWalkPreferences.REMOTE_DEBUGGING, true);
2.桌面Chrome 浏览器
Chrome浏览器地址栏中输入 chrome://inspect/#devices 命令并回车列出所有可调试界面
chrome://inspect/#devices
点击inspect, 打开调试工具, 开始调式
踩坑指南
自动旋转 Activity 被销毁!?
<activity
android:name=".aar.web.activity.WebActivity"
android:label="@string/title_activity_main"
android:theme="@style/Base.Theme.MyApplication"
android:screenOrientation="portrait"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|density|layoutDirection|colorMode"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>当设备从纵向旋转到横向(或反之)时,系统会销毁并重新创建Activity。如果调用外部的Activity 旋转了, 也会导致调用方 (WebActivity) 被销毁的;
android:configChanges属性用于告诉系统,当指定的配置变化发生时,当前的Activity不需要被销毁并重新创建,而是由Activity本身来处理这些变化。
404 或者空白页
原因是chrome 需要下载依赖插件; 然而地址被墙了; SO…(挂代理)
加载资源时 ERR_FILE_NOT_FOUND
net::ERR_FILE_NOT_FOUND; 注意路径, rebuild 一下, 有缓存
加载非https
对于以 Android 6.0(API 级别 23)及更高版本为目标平台的应用,仅 HTTPS 等安全的起点支持 Geolocation API。
如果不调用相应的 onGeolocationPermissionsShowPrompt() 方法,非安全起点向 Geolocation API 发出的任何请求都会被自动拒绝。
配置 android:networkSecurityConfig 属性即可
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
<!-- 注意这里只是引用配置 network_security_config-->
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/Theme.MyApplication"
tools:targetApi="31">
实际配置 network_security_config 白名单
src\main\res\xml\network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">192.168.20.130</domain>
<domain includeSubdomains="true">192.168.20.81</domain>
<domain includeSubdomains="true">192.168.20.82</domain>
<!-- Add other domains if needed -->
</domain-config>
</network-security-config>
忽略https证书验证
private void initWebView() {
XLog.i(TAG+ " initWebView , getVersionName = "+getVersionName());
webView = findViewById(R.id.webView);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
webSettings.setDefaultTextEncodingName("UTF-8");
webSettings.setDomStorageEnabled(true);
webSettings.setAllowFileAccess(true);
WIM = initJavaScriptEngineEnvironment(webView);
/**
* 设置 WebViewClient 来监测页面加载状态
*/
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
if (isFirstLoading) {
isFirstLoading = false;
}
}
// 允许SSL 错误
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();
}
});
mHandler.post(() -> {
webView.loadUrl("file:///android_asset/html/dist/index.html");
// 测试
WebView.setWebContentsDebuggingEnabled(true);
// webView.loadUrl("http://192.168.20.130/android/index.html");
// webView.loadUrl("http://192.168.20.36:8080");
// webView.loadUrl("file:///android_asset/html/test/index.html");
});
}
混合开发 (MixApp)
Vue3 + Element Plus + axios 安卓源码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="./js/axios/axios.min.js"></script>
<script src="./js/vue/vue@3.2.26.js"></script>
</head>
<style>
html,body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#app {
padding: 15px;
width: 100%;
height: 100%;
flex-direction: column;
}
</style>
<body>
<div id="app">
<div>
<button @click="clickEvent">clickEvent</button>
<button @click="clickRequset">clickRequset</button>
</div>
<div>
<div>data.count: {{data.count}} </div>
<div> {{data.res}}</div>
</div>
</div>
</body>
<script type="module" >
import request from './js/utils/request.js'
const app = Vue.createApp(...);
</script>
</html>