Android Scheme方案

在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
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容