文章摘要
白衣 DeepSeek

前言

在移动应用开发中,WebView 是一个强大的工具,它允许我们在应用内展示网页内容,实现与网页的交互。本文将深入探讨 WebView 的各种功能和应用场景,并提供详细的代码示例,帮助开发者全面掌握 WebView 的使用。

一.获取WebView

不同的平台或布局,获取 WebView 的方式有所不同
先说LuaWebView,在自行设置控件时,假设是这样的:

1
2
3
4
5
6
7
8
9
--省略框架
{
LuaWebView,
id="LuaWebView",
layout_width='wrap',
layout_height='350dp',
},
--省略框架
LuaWebView.loadUrl("https://baiyi.ink")--加载网页

那此时我们的WebView应该这样获取并进行设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
local webSettings = LuaWebView.getSettings();
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
--// 禁用缓存
local WebSettings = luajava.bindClass "android.webkit.WebSettings"
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
--// 开启js支持
webSettings.setJavaScriptEnabled(true);

local WebViewClient = luajava.bindClass "android.webkit.WebViewClient"

LuaWebView.setWebViewClient(luajava.override(WebViewClient,{
shouldOverrideUrlLoading=function(superCall,view,webResourceRequest)
--即将开始加载事件
--返回true则拦截本次加载
--拦截加载建议在这里操作
--判断加载的链接
if webResourceRequest.getUrl().toSafeString():find("") then
return true
end
return false
end,
onReceivedSslError=function(superCall, view, sslErrorHandler, sslError)
--ssl证书错误处理事件
--需自行处理,否则在FA2中会导致卡死
--返回true拦截原事件
local sslErr = {
[4] = "SSL_DATE_INVALID\n证书的日期无效",
[1] = "SSL_EXPIRED\n证书已过期",
[2] = "SSL_IDMISMATCH\n主机名称不匹配",
[5] = "SSL_INVALID\n发生一般性错误",
[6] = "SSL_MAX_ERROR\n此常量在API级别14中已弃用。此常数对于使用SslError API不是必需的,并且可以从发行版更改为发行版。",
[0] = "SSL_NOTYETVALID\n证书尚未生效",
[3] = "SSL_UNTRUSTED\n证书颁发机构不受信任"
}
print(sslError.getUrl().."遇到了SSL证书错误,错误类型:"..sslError.getPrimaryError().."\n"..sslErr[sslError.getPrimaryError()])
--忽略错误
sslErrorHandler.proceed()
--取消加载(这是默认行为)
--sslErrorHandler.cancel()
return true
end,
}))

再以 FA2 自带 WebView 为例,监听所有浏览页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
--监听全部浏览器的事件
--由于fa2手册自带的浏览器状态监听智能监听第一个浏览页
--Adam·Eva重新写了个监听所有浏览页的
import "net.fusionapp.core.ui.fragment.WebInterface"
import "androidx.viewpager.widget.ViewPager$OnPageChangeListener"
local uiManager=this.uiManager
local viewPager=uiManager.viewPager
local pagerAdapter=uiManager.pagerAdapter
local pagerCount=pagerAdapter.getCount()
function webInterface()
for i = 0,pagerCount-1,1 do
local fragment=uiManager.getFragment(i)
if fragment then
fragment.setWebInterface(WebInterface{onPageFinished=function(view,url)
--页面加载结束事件
end,
onPageStarted=function(view,url,favicon)
--页面开始加载事件
end,
onReceivedTitle=function(view,title)
--获取到网页标题时加载的事件
end,
onLoadResource=function(view,url)
--页面资源加载监听
--可通过该方法获取网页上的资源
end,
onUrlLoad=function(view,url)
--即将开始加载事件,url参数是即将加载的url
--该函数返回一个布尔值
--返回true则拦截本次加载
return false
end,
onReceivedSslError=function(view, sslErrorHandler, sslError)
--ssl证书错误处理事件
--需自行处理,请返回true拦截原事件
return false
end
})
end
end
end
webInterface()
viewPager.setOnPageChangeListener(OnPageChangeListener{
onPageSelected=function(n)
webInterface()
end
})

二.浏览器控件API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
require "import"
import "android.net.Uri"
import "android.view.View"
import "android.webkit.WebSettings"
import "android.content.Intent"
import "android.widget.LinearLayout"
import "android.widget.Toast"
import "com.androlua.LuaWebView"
import "android.webkit.WebViewClient"
--浏览器控件讲解
layout=--全屏框架
{
LinearLayout;--线性控件
orientation='vertical';--布局方向
layout_width='fill';--布局宽度
layout_height='fill';--布局高度
background='#ffeeeeee';--布局背景
{
LuaWebView;--浏览器控件
layout_width='fill';--浏览器宽度
layout_height='fill';--浏览器高度
id='webView';--控件ID
};

}
activity.setContentView(loadlayout(layout))

import "android.webkit.WebView"
webView.addJavascriptInterface({},"JsInterface")--漏洞封堵代码

--现代 Web 特性支持:
-- 启用现代Web API
webView.getSettings().setDomStorageEnabled(true)
webView.getSettings().setDatabaseEnabled(true)
webView.getSettings().setGeolocationEnabled(true)

-- PWA 支持
webView.getSettings().setAppCacheEnabled(true)
webView.getSettings().setAppCachePath(activity.getCacheDir().toString())

--导航控制增强:
webView.setWebViewClient(luajava.override(WebViewClient, {
shouldOverrideUrlLoading = function(view, request)
-- 深度链接处理示例
if request.url:find("tel:") then
activity.startActivity(Intent(Intent.ACTION_DIAL, Uri.parse(request.url)))
return true
end
return false
end,

onReceivedHttpError = function(view, request, errorResponse)
-- 自定义错误页处理
view.loadUrl("file:///android_asset/error.html")
end
}))

--性能优化方案:
-- 启用现代Web特性
webView.getSettings().setMediaPlaybackRequiresUserGesture(false) -- 自动播放策略
webView.getSettings().setMixedContentMode(2) -- 混合内容处理

-- 内存管理优化
webView.setLayerType(View.LAYER_TYPE_HARDWARE, nil) -- 硬件加速
webView.clearCache(true) -- 定期清理缓存

--安全增强配置
-- 禁用危险接口
webView.removeJavascriptInterface("searchBoxJavaBridge_")
webView.removeJavascriptInterface("accessibility")
webView.removeJavascriptInterface("accessibilityTraversal")

-- 安全JS接口实现
local safeInterface = {
showToast = function(text)
Toast.makeText(activity, text, Toast.LENGTH_SHORT).show()
end
}
webView.addJavascriptInterface(safeInterface, "SafeJsBridge")


--常用API
webView.loadUrl("https://www.baidu.com/")--加载网页
webView.loadUrl("file:///storage/sdcard0/index.html")--加载本地文件

webView.loadUrl("view-source:"..webView.url)--查看网页源码
webView.evaluateJavascript([[JavaScript代码]],nil)--加载JS代码

webView.requestFocusFromTouch()--设置支持获取手势焦点
webView.getSettings().setForceDark(WebSettings.FORCE_DARK_ON)--设置深色模式

webView.getSettings().setSupportZoom(true); --支持网页缩放
webView.getSettings().setBuiltInZoomControls(true); --支持缩放
webView.getSettings().setLoadWithOverviewMode(true);--缩放至屏幕的大小
webView.getSettings().setDisplayZoomControls(false); --隐藏自带的右下角缩放控件


webView.setVerticalScrollBarEnabled(false)--隐藏垂直滚动条

webView.getSettings().setLoadsImagesAutomatically(true);--图片自动加载
webView.getSettings().setUseWideViewPort(true) --图片自适应

webView.setHorizontalScrollBarEnabled(false)--设置是否显示水平滚动条
webView.setVerticalScrollbarOverlay(true)--设置垂直滚动条是否有叠加样式
webView.setScrollBarStyle(webView.SCROLLBARS_OUTSIDE_OVERLAY)--设置滚动条的样式

webView.getSettings().setDomStorageEnabled(true); --dom储存数据
webView.getSettings().setDatabaseEnabled(true); --数据库
webView.getSettings().setAppCacheEnabled(true); --启用缓存
webView.getSettings().setCacheMode(webView.getSettings().LOAD_CACHE_ELSE_NETWORK);--设置缓存加载方式
webView.getSettings().setAllowFileAccess(true);--允许访问文件
webView.getSettings().setSaveFormData(true); --保存表单数据,就是输入框的内容,但并不是全部输入框都会储存
webView.getSettings().setAllowContentAccess(true); --允许访问内容
webView.getSettings().setJavaScriptEnabled(true); --支持js脚本
webView.getSettings().supportMultipleWindows() --设置多窗口
webView.setLayerType(View.LAYER_TYPE_HARDWARE,nil);--硬件加速
webView.getSettings().setPluginsEnabled(true)--支持插件
webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); --//支持通过JS打开新窗口
webView.getSettings().setUserAgentString('Mozilla/5.0 (Linux; Android 10.1.2; Build/NJH47F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.109 Safari/537.36')--设置浏览器标识(UA)
webView.getSettings().setDefaultTextEncodingName("utf-8")--设置编码格式
webView.getSettings().setTextZoom(100)--设置字体大小:100表示正常,120表示文字放大1.2倍
webView.getSettings().setAcceptThirdPartyCookies(true) --接受第三方cookie
webView.getSettings().setSafeBrowsingEnabled(true)--安全浏览
webView.getSettings().setGeolocationEnabled(true);--启用地理定位

webView.goForward()--网页前进
webView.goBack()--网页后退
webView.reload()--刷新网页
webView.stopLoading()--停止加载网页

webView.getTitle()--获取网页标题
webView.getUrl()--获取当前Url
webView.getFavicon()--获得当前网页的图标
webView.getProgress()--获得网页加载进度

--状态监听
webView.setWebViewClient{
shouldOverrideUrlLoading=function(view,url)
--Url即将跳转
end,
onPageStarted=function(view,url,favicon)
--网页即将加载
end,
onPageFinished=function(view,url)
--网页加载完成
end,
onReceivedError=function(view,code,des,url)
--网页加载失败
end,
onLoadResource=function(view,url)
--加载页面资源时
end,
shouldInterceptRequest=function(view,url)
--加载url制定的资源
end,
onReceivedSslError=function(view,handler,err)
--加载SSL证书错误时
end,

}

webView.setWebChromeClient(luajava.override(luajava.bindClass "android.webkit.WebChromeClient",{
onReceivedTitle=function(super,view,title)
--获取到网页标题
end,
onReceivedIcon=function(super,view,title)
--获取到网页图标
end,
onProgressChanged=function(view,progress)
--页面加载进度
end,

}))

webView.setDownloadListener{
onDownloadStart=function(url,userAgent,contentDisposition,mimetype,contentLength)
--即将下载文件时(链接,UA,处理,类型,大小)
local 大小=string.format("%.2f",contentLength/1048576).."MB"

end,

}

三.WebView使用指南

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
--WebView,看这一篇就够了!
--本项目亦可用作以WebView为主的项目的初始模板

local WebChromeClient = luajava.bindClass "android.webkit.WebChromeClient"
local WebViewClient = luajava.bindClass "android.webkit.WebViewClient"
local DownloadListener = luajava.bindClass "android.webkit.DownloadListener"
-- 本项目总结了WebViewClient,WebChromeClient和DownloadListener的常用操作
-- 本项目可以用做使用WebView项目的初始代码
-- WebViewClient主要负责浏览器相关行为
-- WebChromeClient主要负责JS等脚本行为
-- DownloadListener负责浏览器的下载行为
-- 形参中的"superCall"是 luajava.override 返回的是 com.luajava.LuaMethodInterceptor 的内部类 SuperCall 对象,用于调用父类的这个方法
-- author Adam·Eva

--本项目可以告诉你:
-- 1)如何避免SSL错误导致的卡死
-- 2)如何在WebView中执行自己想要的JavaScript脚本,例如通过JS删除指定元素,JS删除广告或其他内容
-- 3)如何禁止用户点击打开不允许打开的链接
-- 4)如何限制网页打开应用,以及获知有没有能打开的应用
-- 5)如何对网页中的原生JS弹窗进行处理
-- 6)如何上传文件
-- 7)如何掌握网页加载的整个流程
-- 8)如何监听下载文件
-- 9)如何获知用户点击了网页中的什么
-- 10)处理网页的定位请求
-- 11)通过CookieManager进行Cookie管理
-- 12)各种异常事件的处理
-- 13)自定义视频全屏和退出的行为和效果

--先拿WebView,根据不同平台或布局,获取方式自行修改
--此处以FA2自带WebView为例
--为方便多浏览页调试不同类型网页写了个循环,maxPage指浏览页的数量
local maxPage=1
for i=0,maxPage-1 do
--上面的i只在下面这行用到了一次
local fragment = activity.getUiManager().getFragment(i)
local webView = fragment.getWebView()
webView.loadUrl("https://httpstat.us/404")
--上传文件页面
--webView.loadUrl("https://imgse.com/")
--视频测试
--webView.loadUrl("http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8")
-- 用于错误页
local errView,errStatus
-- [[WebViewClient,用于浏览器操作
webView.setWebViewClient(luajava.override(WebViewClient,{
shouldOverrideUrlLoading=function(superCall,view,webResourceRequest)
--即将开始加载事件
--返回true则拦截本次加载
--拦截加载建议在这里操作
local String = luajava.bindClass "java.lang.String"
local Intent = luajava.bindClass "android.content.Intent"
local Uri = luajava.bindClass "android.net.Uri"
print("即将开始",webResourceRequest.getMethod(),"加载",webResourceRequest.getUrl().toString())
--判断加载的链接是http/s还是scheme
if not webResourceRequest.getUrl().toSafeString():find("^https?://") then
print("阻止了"..webResourceRequest.getUrl().toSafeString().."的加载\n并尝试启动外部应用")
--给scheme创建一个intent
local intent = Intent(Intent.ACTION_VIEW, Uri.parse(webResourceRequest.getUrl().toString()))
--判断有没有对应的应用能打开这个scheme
pm = activity.getPackageManager()
local componentName = intent.resolveActivity(pm)
if componentName == nil then
print("没有用于打开的应用哦")
else
local PackageManager = luajava.bindClass "android.content.pm.PackageManager"
local pkgname = componentName.getPackageName()
local appname = pm.getApplicationLabel(pm.getApplicationInfo(pkgname,PackageManager.GET_META_DATA))
print("有用于打开的应用哦:\n"..appname.."\n"..pkgname)
--打开它
--activity.startActivity(intent)
end
return true
end
return false
end,
shouldInterceptRequest=function(superCall,view,webResourceRequest)
--控制网页中的资源加载,比如js/css等,并替换为自己的内容
if webResourceRequest.getUrl().getPath():find("png") then
--print("阻止了资源"..webResourceRequest.getUrl().getPath().."的加载")
local WebResourceResponse = luajava.bindClass "android.webkit.WebResourceResponse"
local ByteArrayInputStream = luajava.bindClass "java.io.ByteArrayInputStream"
local String = luajava.bindClass "java.lang.String"
local FileInputStream = luajava.bindClass "java.io.FileInputStream"
--如果要阻止,则应该自己构建响应体以返回
--这里以图片为例,拦截了png图片请求,所以读取了图片的二进制流
--文本可以用java.io.ByteArrayInputStream读取为流
local imgDir = activity.getLoader().getImagesDir("add")
--根据文件类型,记得修改mime类型
return WebResourceResponse("image/png", "UTF-8", FileInputStream(imgDir))
end
return nil
end,
onPageStarted=function(superCall,view,url,favicon)
--页面开始加载事件
--不建议用于禁止加载特定链接再goBack回去,这个行为应当使用shouldOverrideUrlLoading
print(url.."开始加载")
--但是可以把处理网页元素的方法扔在这里,或者onPageFinished里
local ValueCallback = luajava.bindClass "android.webkit.ValueCallback"
--要移除的元素,使用标准CSS选择器语法
--学习参考 https://developer.mozilla.org/zh-CN/docs/Learn/CSS/First_steps
local css = "a"
--使用JS定时器移除元素,原因是有些元素可能会出来的非常晚,或执行某些操作后才出现
view.evaluateJavascript([[
setInterval(() => {
elements = document.querySelectorAll("]]..css..[[")
elements.forEach(e => e.style.display = "none")
// if (elements.length > 0) alert("移除了" + elements.length + "个元素")
},100
)
]],nil)
--因为navigator.permission在 WebView 中未定义
--所以navigator.clipboard.writeText无法获取写权限
--因而考虑传回原生层处理
function copyText(text)
local Context = luajava.bindClass "android.content.Context"
activity.getSystemService(Context.CLIPBOARD_SERVICE).setText(tostring(text))
end
view.evaluateJavascript([[
const oldWrite = navigator.clipboard.writeText
navigator.clipboard.writeText = (text)=>{androlua.callLuaFunction("copyText",text)oldWrite(text)}
]],nil)
end,
onPageFinished=function(superCall,view,url)
--页面加载结束事件
--print(url.."加载结束")
local ValueCallback = luajava.bindClass "android.webkit.ValueCallback"
local Uri = luajava.bindClass "android.net.Uri"
view.evaluateJavascript("((url)=>{alert(url);return url;})('"..url.."加载结束')",
ValueCallback{
onReceiveValue=function(res)
print("evaluateJavascript回调:"..res)
end
})
--获取CookieManager单例来管理cookie
local CookieManager = luajava.bindClass "android.webkit.CookieManager"
local cookieManager = CookieManager.getInstance()
--清空cookie
--让我看看有多少人抄东西不过脑子
cookieManager.removeAllCookie()
--设置cookie
cookieManager.setCookie(url,"testCookie=this is a cookie")
--读取所有cookie
local testCookie = cookieManager.getCookie(url)
print("获取"..Uri.parse(url).toSafeString().."的cookie:"..testCookie)
--有错误页且此次没有错误码
if errView ~= nil and errStatus == nil then
errView.getParent().removeView(errView)
end
end,
onLoadResource=function(superCall,view,url)
--页面资源加载监听
--可通过该方法获取网页上的资源
--print("加载资源: "..url)
end,
onReceivedSslError=function(superCall, view, sslErrorHandler, sslError)
--ssl证书错误处理事件
--需自行处理,否则在FA2中会导致卡死
--返回true拦截原事件
local sslErr = {
[4] = "SSL_DATE_INVALID\n证书的日期无效",
[1] = "SSL_EXPIRED\n证书已过期",
[2] = "SSL_IDMISMATCH\n主机名称不匹配",
[5] = "SSL_INVALID\n发生一般性错误",
[6] = "SSL_MAX_ERROR\n此常量在API级别14中已弃用。此常数对于使用SslError API不是必需的,并且可以从发行版更改为发行版。",
[0] = "SSL_NOTYETVALID\n证书尚未生效",
[3] = "SSL_UNTRUSTED\n证书颁发机构不受信任"
}
print(sslError.getUrl().."遇到了SSL证书错误,错误类型:"..sslError.getPrimaryError().."\n"..sslErr[sslError.getPrimaryError()])
--忽略错误
sslErrorHandler.proceed()
--取消加载(这是默认行为)
--sslErrorHandler.cancel()
return true
end,
onReceivedHttpError=function(superCall, view, webResourceRequest, webResourceResponse)
--请求返回HTTP错误码时
print(webResourceRequest.getUrl().toString().."遇到HTTP错误码:",webResourceResponse.getStatusCode(),"原因:",webResourceResponse.getReasonPhrase(),"响应体:",webResourceResponse.getData())
--只考虑网页主请求
if webResourceRequest.isForMainFrame() then
--以下错误页布局仅在FA2中有效,其他编辑器请自行更换页面
errStatus = webResourceResponse.getStatusCode()
local errPage = luajava.bindClass"net.fusionapp.core.R".layout.web_error_page
local inflater = luajava.bindClass "android.view.LayoutInflater".from(activity)
errView = inflater.inflate(errPage, nil)
view.getParent().addView(errView)
errView.setBackgroundColor(luajava.bindClass "android.graphics.Color".parseColor("#ffffff"))
errView.setOnClickListener(function()
errStatus = nil
-- view.reload()
view.loadUrl("https://www.baidu.com")
end)
end
end,
onReceivedError=function(superCall, view, webResourceRequest,webResourceError)
--页面加载异常事件
print(webResourceRequest.getUrl().toSafeString().."\n加载异常,原因为:\n"..webResourceError.getDescription())
end,
}))--]]

-- 这里定义的变量是用于在onActivityResult中判断来源
local fileRequestCode = 12345
-- 这里定义的变量是用于之后储存onShowCustomView的控件
local customView
-- 这里定义的变量是用于之后储存onShowCustomView时屏幕方向
local orientation
-- 这里定义的变量是用于之后储存onShowFileChooser的回调
local uploadFile
-- [[WebChromeClient,用于页面操作
webView.setWebChromeClient(luajava.override(WebChromeClient,{
onJsTimeout=function(superCall)
--网页中JS执行超时
--停止执行
return true
--继续执行,稍后会再次触发
--return false
end,
onJsAlert=function(superCall,view,url,message,result)
--网页提示弹框
--在onJsAlert中,确认和取消没有区别
--确认
result.confirm()
--取消
--result.cancel()
print("Alert消息: "..message)
return true
end,
onJsConfirm=function(superCall,view,url,message,result)
--网页确认弹框
--确认
result.confirm()
--取消
--result.cancel()
print("Confirm消息: "..message)
return true
end,
onJsPrompt=function(superCall,view,url,message,value,result)
--网页输入弹框
--空确认
--result.confirm()
--取消
--result.cancel()
--确认并返回输入文字
result.confirm("这里是输入的内容")
print("Prompt消息: "..message)
return true
end,
onConsoleMessage=function(superCall,message)
--控制台消息
--print("Console消息: "..message.message())
return true
end,
onReceivedTitle=function(superCall,view,title)
--收到网页标题
activity.uiManager.toolbar.titleText=title
end,
onReceivedIcon=function(superCall,view,bitmap)
--收到网页图标,是个Bitmap对象
end,
onProgressChanged=function(superCall,view,progress)
--网页加载进度变化,下面这句针对FA2,其他编辑器请自行修改
view.parent.getChildAt(2).progress=progress
end,
onShowFileChooser=function(superCall, view, valueCallback, fileChooserParams)
--上传文件
local Intent = luajava.bindClass "android.content.Intent"
--保存回调
uploadFile=valueCallback
--从fileChooserParams里取出Intent对象
--这里的Intent对象已经设置好了参数
local intent = fileChooserParams.createIntent()
--允许多选
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE,true)
activity.startActivityForResult(intent, fileRequestCode)
return true
end,
onGeolocationPermissionsShowPrompt=function(superCall, origin, callback)
--收到来自网页的定位请求,自API24起,仅支持安全请求(https)调用,非安全(http)会直接拒绝且不调用此方法
print("请求定位:"..origin)
--参数依次为来源,是否允许,是否记住(String origin, boolean allow, boolean remember)
--可以在这个情况弹窗询问用户
--别忘了给你APP申请定位权限!
callback.invoke(origin,true,false)
end,
onPermissionRequest=function(superCall,permissionRequest)
local Arrays = luajava.bindClass "java.util.Arrays"
print("来自网页"..permissionRequest.getOrigin().toSafeString().."的权限请求:",Arrays.asList(permissionRequest.getResources()))
--搭配原生的权限请求函数使用
--同意所有
--permissionRequest.grant(permissionRequest.getResources())
--拒绝所有(默认行为)
permissionRequest.deny()
end,
onShowCustomView=function(superCall,view,callback)
--重写 WebChromeClient 必须重写这个方法,否则影响视频全屏
--Android开发者文档:如果这个方法没有被覆盖,WebView 将向网页报告它不支持全屏模式,并且不会接受网页在全屏模式下运行的请求。
--视频全屏操作会执行这个方法,想要自己做全屏的可以在这里处理
--callback 对象是 WebChromeClient.CustomViewCallback
print("视频全屏")
print(view.getClass())
--保存一下屏幕方向。
orientation = activity.getRequestedOrientation() or 0
-- 设置跟随传感方向
local ActivityInfo = luajava.bindClass "android.content.pm.ActivityInfo"
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR)
--保存view供之后退出全屏时移除
customView = view
--覆盖在根布局上面
local FrameLayout = luajava.bindClass "android.widget.FrameLayout"
local Gravity = luajava.bindClass "android.view.Gravity"
local lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
activity.addContentView(view,lp)
end,
onHideCustomView=function(superCall)
--视频退出全屏触发的方法,重写onShowCustomView必须同时重写这个方法
print("退出全屏")
--设置为原来的方向
activity.setRequestedOrientation(orientation)
--移除view
customView.getParent().removeView(customView)
--变量置空
customView = nil
orientation = nil
end
}))--]]

--上传文件的回调在这里执行
onActivityResult=function(requestCode,resultCode,intent)
local Activity = luajava.bindClass "android.app.Activity"
local Uri = luajava.bindClass "android.net.Uri"
--如果是来自onShowFileChooser
if requestCode == fileRequestCode then
--如果通过返回键回到应用
if resultCode == Activity.RESULT_CANCELED then
--并且存在上传回调
if uploadFile~=nil then
--就返回空
uploadFile.onReceiveValue(nil)
return
end
end
--定义结果变量,主要是为了确定作用域
local results
--如果正常返回
if resultCode == Activity.RESULT_OK then
--但是回调对象不对
if uploadFile==nil or type(uploadFile)=="number" then
--就直接结束
return
end
--如果返回Intent对象非空
if intent ~= nil then
--拿到数据
local dataString = intent.getDataString()
local clipData = intent.getClipData()
if clipData ~= nil then
--结果转成定长Uri数组
results = Uri[clipData.getItemCount()]
--遍历并储存
for i = 0,clipData.getItemCount()-1 do
local item = clipData.getItemAt(i)
results[i] = item.getUri()
end
end
if dataString ~= nil then
results = Uri[1]
results[0]=Uri.parse(dataString)
end
end
end
if results~=nil then
--返回选择结果
uploadFile.onReceiveValue(results)
uploadFile = nil
end
end
end

webView.setDownloadListener(DownloadListener{
onDownloadStart=function(url, userAgent, contentDisposition, mimetype, contentLength)
-- 详细的下载调用DownloadManager的代码我写的太长了
-- 所以请参见我的“各种下载”项目
print("发现下载行为,\n文件描述为: "..contentDisposition.."\n文件类型为: "..mimetype.."\n文件大小为"..contentLength.."\n下载链接是: "..url)
end
})

--长按获取点击的元素(点击同理)
webView.onLongClick=function()
local WebView = luajava.bindClass "android.webkit.WebView"
--print(webView.getContentHeight())
--print(webView.onCheckIsTextEditor())
local htr=webView.getHitTestResult()
--print(htr.getType(),htr.getExtra())
--通过type获得点击的类型,这里以图片为例
if htr.getType()==WebView.HitTestResult.IMAGE_TYPE then
local imageName=htr.getExtra():match("^.+/(.-)$")
print(htr.getExtra(),imageName)
--自己去下载就完事了,不会的话,参考我的WebView各种下载项目
end
end

--由于WebView默认有自己的按键事件
--当这个事件与我们activity的onKeyDown等冲突时
--可以重写此方法
local View = luajava.bindClass "android.view.View"
webView.setOnKeyListener(View.OnKeyListener{
onKey=function(v, keyCode, event)
local KeyEvent = luajava.bindClass "android.view.KeyEvent"
--判断按键事件为“按下”
if event.getAction() == KeyEvent.ACTION_DOWN then
--判断按的是返回键并且能回退网页
if keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack() then
--这里放你的操作
webView.goBack()
--直接返回true表明已经执行过返回键的操作
return true
end
end
--其它按键依旧按默认处理
return false
end
})
end

--[[
附记:页面加载回调顺序
shouldOverrideUrlLoading
onProgressChanged[10]
shouldInterceptRequest
onProgressChanged[...]
onPageStarted
onProgressChanged[...]
onLoadResource
onProgressChanged[...]
onReceivedTitle/onPageCommitVisible
onProgressChanged[100]
onPageFinished
onReceivedIcon
]]

四.网页暗黑模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function isNightMode() 
local Configuration=luajava.bindClass"android.content.res.Configuration"
currentNightMode = activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK
return currentNightMode == Configuration.UI_MODE_NIGHT_YES--夜间模式启用
end

-- 请浏览
-- https://developer.android.google.cn/develop/ui/views/layout/webapps/dark-theme?authuser=0&hl=zh-cn
-- 获取更多信息

-- 允许使用算法调暗功能(以 Android 13 或更高版本为目标平台的应用)(targetSdkVersion至少是33)
webView.getSettings().setAlgorithmicDarkeningAllowed(true)


-- 允许使用算法调暗功能(以 Android 12 或更低版本为目标平台的应用)(targetSdkVersion="32"或以下)
import "androidx.webkit.WebSettingsCompat"

if isNightMode() then -- 也可以使用自己的相关的业务来判断是否启用
-- 启用
WebSettingsCompat.setForceDark(webView.getSettings(),WebSettingsCompat.FORCE_DARK_ON)
else
WebSettingsCompat.setForceDark(webView.getSettings(),WebSettingsCompat.FORCE_DARK_OFF)
end

五.监听下载与上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
--常用下载功能,包括直链下载,网页点击下载,下载进度监听等
--支持blob协议/data协议下载
--完善了文件名的逻辑
--如果需要修改储存路径,请阅读注释
--来源:Adam·Eva

require "import"
import "android.app.DownloadManager"
import "android.app.ProgressDialog"
import "android.content.Context"
import "android.net.Uri"
import "android.os.Environment"
import "android.util.Base64"
import "android.webkit.DownloadListener"
import "android.webkit.MimeTypeMap"
import "android.webkit.URLUtil"
import "com.androlua.Ticker"
import "java.io.File"
import "java.lang.System"
import "java.net.URLDecoder"
import "android.os.Build"

local uiManager=activity.getUiManager()
local webView=uiManager.getCurrentFragment().getWebView()

--http测试
webView.loadUrl("http://tool.liumingye.cn/music/?page=audioPage&type=migu&name=%E8%A7%A3%E8%8D%AF")
--blob测试
webView.loadUrl("https://app.xunjiepdf.com/text2voice/")
--data测试
--webView.loadUrl("")
--response只返回了URL的网站
--webView.loadUrl("http://music.vaiwan.com/")
--webView.loadUrl("https://viayoo.com")
--欢迎补充测试用例


--如果是点击网页中的下载按钮
webView.setDownloadListener(DownloadListener{
onDownloadStart=function(url, userAgent, contentDisposition, mimetype, contentLength)
--activity.getSystemService(Context.CLIPBOARD_SERVICE).setText(url)
print(url, contentDisposition, mimetype, contentLength)
local ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimetype)
local filename = URLUtil.guessFileName(url,contentDisposition,mimetype)
--受mime影响识别错误时再尝试一下
if filename:match("%.bin$") then
filename = URLUtil.guessFileName(url,contentDisposition,nil)
ext = filename:match(".+%.(.+)$") or ext
end
--print("guess",filename,ext)
webView.evaluateJavascript("document.querySelector('[href=\"" .. url .. "\"]').download", function(result)
--文件名处理
if result ~= nil and result ~= "\"\"" and result ~= "null" then
filename = result:gsub("\"", "") or filename
if MimeTypeMap.getSingleton().hasExtension(filename:match(".+%.(.+)$")) then
ext = filename:match(".+%.(.+)$") or ext
end
end
--print("js",filename,ext)
filename=URLDecoder.decode(filename)
if filename == nil or filename == "" then
filename = tostring(System.currentTimeMillis())
end
if not filename:match("%."..ext.."$") then
filename = filename.."."..ext
end
print(filename)
--路径处理
--在这里设置储存设置路径,如Download
local fullPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
local path = fullPath
if Build.VERSION.SDK_INT >= 29 then --Build.VERSION_CODES.Q
path = Environment.DIRECTORY_DOWNLOADS
end
local file = File(fullPath,filename)
--是否覆盖下载
file.delete()
--base64解码输出,这个方法需要文件读写权限,记得申请
function base64write(data)
make_Snackbar("下载开始",nil,nil)
file.createNewFile()
local Bdata=Base64.decode(data,Base64.DEFAULT)
local output=activity.ContentResolver.openOutputStream(Uri.fromFile(file))
output.write(Bdata)
output.close()
make_Snackbar("下载结束",nil,nil)
end
--根据下载链接类型选择下载方式
--DownloadManager只能下载http/https协议的链接
if URLUtil.isHttpUrl(url) or URLUtil.isHttpsUrl(url) then
make_dialog("下载", filename, nil, "取消", "保存", nil, nil, function()
if Build.VERSION.SDK_INT >= 29 then --Build.VERSION_CODES.Q
download(url, path, filename)
else
download(url, fullPath, filename)
end
end)
--其他的要自己实现
elseif url:find("^blob") then
function blob()
local JSblob=[[
fetch(']]..url..[[', {
responseType: 'blob'
}).then((res) = >{
res.blob().then((data) = >{
data.arrayBuffer().then((buffer) = >{
window.androlua.callLuaFunction("base64write", btoa(String.fromCharCode(...new Uint8Array(buffer))))
})
})
})
]]
webView.loadUrl('javascript:'..JSblob)
end
make_dialog("下载",filename,"取消","保存",nil,nil,function() blob() end)
elseif URLUtil.isDataUrl(url) then
--是否base64编码
if url:find("^data:(.-)(;base64),(.-)$") then
local mime,data=url:match("^data:(.-);base64,(.-)$")
base64write(data)
else
local mime,data=url:match("^data:(.-),(.-)$")
io.open(path,"w+"):write(URLDecoder.decode(data)):close()
end
else
--不认识的协议?那没办法
make_Snackbar("您遇到了未知的下载类型,请去浏览器尝试下载","打开",function() openExtUrl(webView.getUrl()) end)
end
end)
end
})


--监听下载进度,并使用ProgressDialog和定时器显示进度,如果ProgressDialog不好看你可以自己改改
--参数:url是文件直链,path是目录,file是文件名
function download(url, filepath, filename)
local downloadId = downloadFile(url, filepath, filename)
local dlDialog = ProgressDialog(this)
dlDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
--设置进度条的形式为水平进度条
dlDialog.setTitle("即将开始下载...")
dlDialog.setCancelable(false)--设置是否可以通过点击Back键取消
dlDialog.setCanceledOnTouchOutside(false)--设置在点击Dialog外是否取消Dialog进度条
dlDialog.setOnCancelListener{
onCancel=function(l)
make_Snackbar("将进入后台下载,请在通知栏检查下载进度",nil,nil)
end}
dlDialog.setMax(1)
dlDialog.setProgress(0)
dlDialog.show()

local ti=Ticker()
ti.Period=100
ti.onTick=function()
--事件
local fileUri,status,totalSize,downloadedSize = query(downloadId)
if totalSize and totalSize>0 then
if status==1 then
--等待下载
dlDialog.setTitle("等待下载...")
--print("等待下载")
elseif status==2 then
--正在下载
dlDialog.setTitle("正在下载...")
dlDialog.setMax(totalSize)
dlDialog.setProgress(downloadedSize)
elseif status==4 then
--下载暂停
dlDialog.setTitle("下载暂停...")
--print("下载暂停")
elseif status==8 then
--下载成功
dlDialog.setTitle("下载成功")
dlDialog.setMax(totalSize)
dlDialog.setProgress(totalSize)
make_Snackbar("下载成功","打开",function()openExtUrl(fileUri)end)
dlDialog.dismiss()
ti.stop()
elseif status==16 then
--下载失败
dlDialog.dismiss()
make_Snackbar("下载失败,请重试",nil,nil)
ti.stop()
end
end
end
ti.start()--启动Ticker定时器
end
--参数:url是文件直链,path是目录,file是文件名
function downloadFile(url, path, filename)
local downloadManager=activity.getSystemService(Context.DOWNLOAD_SERVICE)
local dlurl=Uri.parse(url)
local request=DownloadManager.Request(dlurl)
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE|DownloadManager.Request.NETWORK_WIFI)
request.setDestinationInExternalPublicDir(path, filename)
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)--|DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
request.setVisibleInDownloadsUi(true)
--如果下载链接有referer验证,加上这句
--request.addRequestHeader("referer",验证域名)
local downloadId=downloadManager.enqueue(request)
return downloadId
end
--查询状态
function query(downloadId)
local downloadManager=activity.getSystemService(Context.DOWNLOAD_SERVICE);
local downloadQuery = DownloadManager.Query();
downloadQuery.setFilterById({downloadId});
local cursor = downloadManager.query(downloadQuery);
if (cursor != null and cursor.moveToFirst()) then
local fileUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
-- 下载请求的状态
local status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
-- 下载文件的总字节大小
local totalSize = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
-- 已下载的字节大小
local downloadedSize = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
cursor.close()
return fileUri,status,totalSize,downloadedSize
end
end

--下面是其他相关调用的函数,你可以按需使用

--弹窗的
function make_dialog(title, text, NeutralButton, NegativeButton, PositiveButton, funa, funb, func)
import "androidx.appcompat.app.AlertDialog"
if not activity.isFinishing() then
if funa==nil then
funa=function() end
end
if funb==nil then
funb=function() end
end
if func==nil then
func=function() end
end
dialog=AlertDialog.Builder(activity)
.setTitle(title)
.setMessage(text)
.setCancelable(false)
.setNeutralButton(NeutralButton,{
onClick=funa
})
.setNegativeButton(NegativeButton,{
onClick=funb
})
.setPositiveButton(PositiveButton,{
onClick=func
})
.show()
end
end

--弹出信息的
function make_Snackbar(text, btn, fun)
import "com.google.android.material.snackbar.Snackbar"
import "android.view.View"
if not activity.isFinishing() then
if btn==nil then
btn="确定"
end
if fun==nil then
fun=function(v) end
end
local anchor=activity.findViewById(android.R.id.content)
Snackbar.make(anchor, text, Snackbar.LENGTH_LONG).setAction(btn, View.OnClickListener{
onClick=fun
}).show()
end
end

--浏览器打开链接的
function openExtUrl(extURL)
import "android.content.Intent"
import "android.webkit.MimeTypeMap"
import "androidx.core.content.FileProvider"
local intent = Intent(Intent.ACTION_VIEW)
local fileUri = Uri.parse(extURL)
if Build.VERSION.SDK_INT >= 29 and fileUri.getScheme() == "file" then
local extName = extURL:match(".+%.(.-)$")
local mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extName)
local authorities = activity.getPackageName()..".FileProvider"
fileUri = FileProvider.getUriForFile(activity, authorities, File(extURL))
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
end
intent.setDataAndType(extURL, mime)
activity.startActivity(intent)
end


--WebView上传文件
--补充多选支持
--Source: Adam·Eva

local uiManager=activity.getUiManager()
local webView=uiManager.getCurrentFragment().getWebView()
-- [[
local WebChromeClient = luajava.bindClass "android.webkit.WebChromeClient"
webView.setWebChromeClient(luajava.override(WebChromeClient,{
onShowFileChooser=function(a, view, valueCallback, fileChooserParams)
--print(a, view, valueCallback, fileChooserParams)
uploadFile=valueCallback
local intent = fileChooserParams.createIntent()
activity.startActivityForResult(intent, 1);
return true;
end,
}))
--]]
onActivityResult=function(req,res,intent)
local Activity = luajava.bindClass "android.app.Activity"
local Uri = luajava.bindClass "android.net.Uri"
if res == Activity.RESULT_CANCELED then
if uploadFile~=nil then
uploadFile.onReceiveValue(nil);
end
end
local results
if res == Activity.RESULT_OK then
if uploadFile==nil or type(uploadFile)=="number" then
return;
end
if intent ~= nil then
local dataString = intent.getDataString();
local clipData = intent.getClipData();
if clipData ~= nil then
results = Uri[clipData.getItemCount()];
for i = 0,clipData.getItemCount()-1 do
local item = clipData.getItemAt(i);
results[i] = item.getUri();
end
end
if dataString ~= nil then
results = Uri[1];
results[0]=Uri.parse(dataString)
end
end
end
if results~=nil then
uploadFile.onReceiveValue(results);
uploadFile = nil;
end
end