5496 字
27 分钟
WebView 开发指南:从基础到高级应用
前言
在移动应用开发中,WebView 是一个强大的工具,它允许我们在应用内展示网页内容,实现与网页的交互。本文将深入探讨 WebView 的各种功能和应用场景,并提供详细的代码示例,帮助开发者全面掌握 WebView 的使用。
一.获取WebView
不同的平台或布局,获取 WebView 的方式有所不同。 先说LuaWebView,在自行设置控件时,假设是这样的:
--省略框架 { LuaWebView, id="LuaWebView", layout_width='wrap', layout_height='350dp', },--省略框架LuaWebView.loadUrl("https://baiyi.ink")--加载网页那此时我们的WebView应该这样获取并进行设置:
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 为例,监听所有浏览页:
--监听全部浏览器的事件--由于fa2手册自带的浏览器状态监听智能监听第一个浏览页--Adam·Eva重新写了个监听所有浏览页的import "net.fusionapp.core.ui.fragment.WebInterface"import "androidx.viewpager.widget.ViewPager$OnPageChangeListener"local uiManager=this.uiManagerlocal viewPager=uiManager.viewPagerlocal pagerAdapter=uiManager.pagerAdapterlocal 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 endendwebInterface()viewPager.setOnPageChangeListener(OnPageChangeListener{ onPageSelected=function(n) webInterface() end})二.浏览器控件API
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 APIwebView.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")
--常用APIwebView.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) --接受第三方cookiewebView.getSettings().setSafeBrowsingEnabled(true)--安全浏览webView.getSettings().setGeolocationEnabled(true);--启用地理定位
webView.goForward()--网页前进webView.goBack()--网页后退webView.reload()--刷新网页webView.stopLoading()--停止加载网页
webView.getTitle()--获取网页标题webView.getUrl()--获取当前UrlwebView.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使用指南
--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=1for 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
--[[附记:页面加载回调顺序shouldOverrideUrlLoadingonProgressChanged[10]shouldInterceptRequestonProgressChanged[...]onPageStartedonProgressChanged[...]onLoadResourceonProgressChanged[...]onReceivedTitle/onPageCommitVisibleonProgressChanged[100]onPageFinishedonReceivedIcon]]四.网页暗黑模式
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五.监听下载与上传
--常用下载功能,包括直链下载,网页点击下载,下载进度监听等--支持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 downloadIdend--查询状态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 endend
--下面是其他相关调用的函数,你可以按需使用
--弹窗的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() endend
--弹出信息的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() endend
--浏览器打开链接的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; endend WebView 开发指南:从基础到高级应用
https://jizhiben.xin/posts/webview-development-guide/ 部分信息可能已经过时









