在Android開(kāi)發(fā)的過(guò)程中,很多活動(dòng)頁(yè)面都使用H5進(jìn)行快速開(kāi)發(fā)并隨時(shí)更新,這樣的話H5與原生的交互就不可避免
H5相關(guān)除了Webview類以外,在使用的過(guò)程中還需要用到別的類:
1.WebSettings 從WebView中g(shù)et,進(jìn)行一些設(shè)置,比如開(kāi)啟js支持,viewport支持(該配置幫助H5進(jìn)行不同屏幕分辨率的適配),使用網(wǎng)頁(yè)自身縮放,dom存儲(chǔ),數(shù)據(jù)庫(kù),多窗口等
2.WebViewClient 主要功能組件,頁(yè)面加載完成,頁(yè)面結(jié)束,頁(yè)面錯(cuò)誤等回調(diào),最主要的是通過(guò)shouldOverrideUrlLoading方法對(duì)url進(jìn)行攔截以進(jìn)行客戶端需要的一些操作,也在該方法中進(jìn)行scheme的調(diào)用,另外還有其他的資源攔截方法可以用來(lái)緩存
3.WebChromeClient 處理關(guān)于外層的一些事物,如網(wǎng)站標(biāo)題,Icon,加載進(jìn)度,定位權(quán)限請(qǐng)求等
在H5與原生的交互中,原生調(diào)用H5比較容易,直接使用以下的代碼即可
WebView.loadUrl("javascript:XXX")
對(duì)于H5調(diào)用來(lái)說(shuō)一般有兩種方法,一個(gè)是通過(guò)webview.addJavascriptInterface(Object,String)和@JavascriptInterface注解使H5可以直接調(diào)用原生的某些方法,但這樣做的局限性比較大,不太推薦
另一種方法就是使用scheme:
Android Scheme主要是用來(lái)H5與原生頁(yè)面的交互,而且這種使用url的交互也可以用于原生,后端返回不同的鏈接客戶端做出不同的操作,非常靈活,在信鴿的推送點(diǎn)擊中也很好用
對(duì)于這個(gè)需求主要分為以下幾個(gè)步驟:
1.監(jiān)聽(tīng)webview發(fā)出的請(qǐng)求
2.對(duì)該請(qǐng)求進(jìn)行解析
3.對(duì)解析結(jié)果進(jìn)行相應(yīng)的操作
1.監(jiān)聽(tīng)并攔截webview的請(qǐng)求
對(duì)于webview請(qǐng)求相關(guān)的回調(diào)都存在于WebViewClient中
需要完成H5調(diào)用需要H5發(fā)起一個(gè)url請(qǐng)求,客戶端在WebViewClient.shouldOverrideUrlLoading()方法中進(jìn)行攔截
該方法分為兩個(gè)版本,如下:
@RequiresApi(Build.VERSION_CODES.N)//這個(gè)注解只是個(gè)提示,起不到過(guò)濾版本的作用,還是需要進(jìn)行版本判斷,否則高版本會(huì)兩個(gè)方法都調(diào)用,N以下版本沒(méi)有這個(gè)方法
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val url = request.url.toString()
if (url.startsWith(HTTP_URL_PREFIX)) {//不攔截http請(qǐng)求,這里不攔截的話會(huì)在同一個(gè)webview中打開(kāi)新的頁(yè)面,如有需求可以進(jìn)行攔截并使其打開(kāi)新的頁(yè)面
return super.shouldOverrideUrlLoading(view, request)
}
return SchemeUtil.openScheme(activity!!, url, fragment)//處理其他請(qǐng)求
}
return false//返回false不攔截
}
@Suppress("deprecation")
override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
if (url.startsWith(HTTP_URL_PREFIX)) {
return super.shouldOverrideUrlLoading(view, url)
}
return SchemeUtil.openScheme(activity!!, url, fragment)
}
return false
}
2.解析請(qǐng)求
第一步里過(guò)濾了http請(qǐng)求不進(jìn)行scheme的相關(guān)流程
所以需要進(jìn)行scheme調(diào)用的就前端和客戶端約定一套schme,可以使用 項(xiàng)目名://+方法名+.項(xiàng)目名?json_params={} 格式,加兩個(gè)項(xiàng)目名來(lái)過(guò)濾以防錯(cuò)誤調(diào)用(保險(xiǎn)的話可以用更復(fù)雜的名字或者加密,一般不太需要)
然后就是對(duì)某個(gè)具體的scheme進(jìn)行解析:
//第一步解析url獲取參數(shù),protocolStr為完成的scheme鏈接,fragment為發(fā)起請(qǐng)求的fragment,對(duì)于webview來(lái)說(shuō)可以在創(chuàng)建webviewclient時(shí)將activity和fragment傳進(jìn)來(lái)方便后續(xù)使用
//這個(gè)方法就是將scheme解析為{String方法名}和{JSONObject參數(shù)}
fun openScheme(context: Context,protocolStr:String,fragment: Fragment?){
//防抖
if(System.currentTimeMillis() - lastTime < GAP_OF_SAME_SCHEME
&& TextUtils.isEmpty(protocolStr)
&& protocolStr == lastHost)return
lastTime = System.currentTimeMillis()
lastHost = protocolStr
/**
* foo://example.com:8042/over/there?name=ferret&psw=123#nose
* \_/ \______________/\_________/ \_________________/ \__/
* | | | | |
* scheme authority path query fragment
*
*
*/
try {
val uri = URI(protocolStr)
val host = uri.authority
val query = protocolStr.substring(protocolStr.indexOf('?') + 1)
var paramObj:JSONObject? = null
if(query!=null) {
val paramStr = query.split("&")
val paramMap = HashMap<String, String>()
for (pairStr in paramStr) {
val pair = pairStr.split(Regex("="),2)
if (pair.size == 2) {
paramMap[pair[0]] = StringUtil.decodeString(pair[1])
}
}
// 獲取json_params參數(shù)
val json = paramMap[mSchemeKeyParams]
paramObj = if (json == null) null else JSONObject(json)
}
var type:String? = null
if (host!=null && host.indexOf(".") != -1){
type = host.substring(0,host.indexOf("."))
}
openScheme(context,type,paramObj,fragment)
}catch (e: Exception){
e.printStackTrace()
}
}
3.對(duì)解析結(jié)果進(jìn)行處理
這里拿到方法名和參數(shù)以后就可以進(jìn)行對(duì)應(yīng)的操作
有很多方法可以進(jìn)行,最直接的方法可以使用switch-case在原地進(jìn)行處理,但這樣就是造成一個(gè)類過(guò)于龐大導(dǎo)致可讀性可維護(hù)性下降,而且即使調(diào)用別的類的方法也需要每次添加scheme都在這里修改,不符合開(kāi)閉原則
這里推薦使用反射,具體操作如下:
1.新建一個(gè)scheme處理接口,比如:
interface ISchemeFilter {
fun process(context: Context, paramObj: JSONObject?, fragment: Fragment?)
}
2.創(chuàng)建一個(gè)上面接口的實(shí)現(xiàn)類,比如:
class OpenXXXFilter:ISchemeFilter {
override fun process(context: Context, paramObj: JSONObject?, fragment: Fragment?) {
XXXActivity.open(context)
//如果簡(jiǎn)單的操作在這里可以完成則到此結(jié)束,如果不行則使用context和fragment進(jìn)行相應(yīng)方法的調(diào)用
}
}
3.建立一個(gè)Map,以方法名為鍵,具體實(shí)現(xiàn)類的類名 //這個(gè)可以不用,可以直接使用方法名作為類名,但是不太靈活
4.通過(guò)方法名查詢到對(duì)應(yīng)的類名,并使用該類名創(chuàng)建實(shí)例并調(diào)用其process方法,具體代碼如下:
val schemeFilterClassName = schemeMap[type]
schemeFilterClass = Class.forName(schemeFilterClassName!!)
val constructor = schemeFilterClass!!.getConstructor()
val schemeFilter = constructor.newInstance() as ISchemeFilter
schemeFilter.process(context, paramObj, fragment)
//注意判空與try-catch