標籤:android webviewclient webchromeclient 同步 非同步
大家都知道,通過WebView,我們可以在Android用戶端,用Web開發的方式來開發我們的應用。
如果一個應用就是單純一個WebView,所有的邏輯都只需要在網頁上互動的話,那我們其實就只需要通過html和javascript來跟伺服器互動就可以了。
但是很多情況下,我們的應用不是單純一個WebView就可以了,有可能會需要運用到Android本身的應用,比如拍照,就需要調用Android本身的照像機等,要產生震動,在需要運用到手機特性的一些情境下,肯定需要這麼一套機制在javascript和Android之間互相通訊,包括同步和非同步方式,而這套機制就是本文中我想要介紹的。
一步一步來,我們先從最簡單的地方講起:
1)需要一個WebView去展現我們的頁面,首先定義一個布局,非常簡單,就是一個WebView,如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "match_parent" android:orientation= "vertical"> <WebView android:id="@+id/html5_webview" android:layout_width="match_parent" android:layout_height="match_parent" /></LinearLayout>
這個WebView就是承載我們頁面展現的一個最基本的控制項,所有在頁面上的邏輯,需要跟Android原生環境互動的邏輯資料都是通過它來傳輸的。
2)在對應的Activity中,對WebView進行一些初始化
mWebView = (WebView) findViewById(R.id. html5_webview );WebSettings webSettings = mWebView.getSettings();webSettings.setJavaScriptCanOpenWindowsAutomatically( true );webSettings.setJavaScriptEnabled( true );webSettings.setLayoutAlgorithm(LayoutAlgorithm. NORMAL );mWebView.setWebChromeClient( new WebServerChromeClient());mWebView.setWebViewClient( new WebServerViewClient());mWebView.setVerticalScrollBarEnabled( false );mWebView.requestFocusFromTouch();mWebView.addJavascriptInterface( new AppJavascriptInterface(), "nintf" );
在上面的代碼中,主要是對WebView的一些初始化,但其中最重要的幾句代碼是這麼幾句:
2.1)webSettings.setJavaScriptEnabled( true );
告訴WebView,讓它能夠去執行JavaScript語句。在一個互動的網頁上,javascript是沒辦法忽略的。
2.2)mWebView.setWebChromeClient( new WebServerChromeClient());2.3)mWebView.setWebViewClient( new WebServerViewClient());
WebChromeClient和WebViewClient是WebView應用中的兩個最重要的類。
通過這兩個類,WebView能夠捕獲到Html頁面中url的載入,javascript的執行等的所有操作,從而能夠在Android的原生環境中對這些來自網頁上的事件進行判斷,解析,然後將對應的處理結果返回給html網頁。
這兩個類是html頁面和Android原生環境互動的基礎,所有通過html頁面來跟後台互動的操作,都在這兩個類裡面實現,在後面我們還會詳細說明。
2.4)mWebView.addJavascriptInterface( new AppJavascriptInterface(), "nintf" );
這個JavascriptInterface,則是Android原生環境和javascript互動的另一個視窗。
將我們自訂的AppJavascriptInterface類,調用mWebView的addJavascriptInterface方法,可以將這個對象傳遞給mWebView中Window對象的nintf屬性("nintf"這個屬性名稱是自訂的)之後,
就可以直接在javascript中調用這個Java對象的方法。
3)接下來,我們就先來看看在Html中的javascript是如何跟Android原生環境來互動的。
我們按照事件發生的順序機制來看,這樣有個先後的概念,理解起來會容易一點。
在這套機制中,提供了兩種訪問Android原生環境的方法,一種是同步的,一種是非同步。
同步的概念就是說,我在跟你交流的時候,如果我還沒有收到你的回複,我是不能跟其他人交流的,我必須等在那裡,一直等著你。
非同步概念就是說,我在跟你交流的時候,如果你還沒有回複我,我還能夠去跟其他人交流,而當我收到你的回複的時候,再去看看你的回複,應該要幹些什麼。
3.1)同步訪問
在Javascript中,我們定義了這樣一個方法,如下:
var exec = function (service, action, args) { var json = { "service" : service, "action" : action }; var result_str = prompt(JSON.stringify(json), args); var result; try { result = JSON.parse(result_str); } catch (e) { console.error(e.message); } var status = result.status; var message = result.message; if (status == 0) { return message; } else { console.error( "service:" + service + " action:" + action + " error:" + message); }}
而對此方法的,典型的調用如下:
exec( "Toast", "makeTextShort" , JSON.stringify(text));
其中Toast和makeTextShort是要調用Android原生環境的服務和參數,這些都是在PluginManager中管理的,在下一篇文章中會提及到。
在這裡,我們調用了prompt方法,通過這個方法,在WebView中定義的的WebChromeClient就會攔截到這樣一個方法,具體代碼如下:
class WebServerChromeClient extends WebChromeClient { @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { System.out.println( "onJsPrompt:defaultValue:" + defaultValue + "|" + url + "," + message); JSONObject args = null ; JSONObject head = null ; try { head = new JSONObject(message); args = new JSONObject(defaultValue); String execResult = mPluginManager.exec(head.getString(IPlugin.SERVICE), head.getString(IPlugin.ACTION), args); result.confirm(execResult); return true; ... } }
在這裡,我們會重載WebChromeClient的onJsPrompt方法,當此方法返回true的時候,就說明WebChromeClient已經處理了這個prompt事件,不需要再繼續分發下去;
而當返回false的時候,則此事件會繼續傳遞給WebView,由WebView來處理。
由於我們這裡是要利用這個Prompt方法,來實現Javascript跟Android原生環境之間的同步訪問,所以我們在這裡會攔截這個事件進行處理。
在這裡,通過message和defaultValue,我們可以拿到javascript中prompt方法兩個參數的值,在這裡,它們是Json資料,在這裡進行解析之後,由PluginManager來進行處理,最後將結果返回給JsPromptResult的confirm方法中。
此結果就是javascript中prompt的返回值了。
而除了JsPrompt,還有類似Javascript中的Alert方法等,我們知道瀏覽器彈出的Alert視窗跟我們手機應用中視窗風格樣式是很不一樣的,而作為一個應用,風格肯定要有一套統一的標準,所以一般情況下,我們也會攔截WebView中的Alert視窗,這個邏輯也同樣會是在這裡處理,如下:
@Overridepublic boolean onJsAlert(WebView view, String url, String message, final JsResult result) { System. out .println("onJsAlert : url:" + url + " | message:" + message); if (isFinishing()) { return true ; } CustomAlertDialog.Builder customBuilderres = new CustomAlertDialog.Builder(DroidHtml5.this ); customBuilderres.setTitle( "資訊提示" ).setMessage(message) .setPositiveButton( "確定" , new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); result.confirm(); } }).create().show(); return true ;}
上面描述的都是同步訪問Android原生環境的方式,那麼,非同步訪問方式是怎麼樣的呢?
3.2)非同步訪問
同樣的,我們會在Javascript中定義如下一個方法:
var exec_asyn = function(service, action, args, success, fail) { var json = { "service" : service, "action" : action }; var result = AndroidHtml5.callNative(json, args, success, fail); }
我們會調用AndroidHtml5的callNative,此方法有四個參數:
a)json:是調用的服務和操作
b)args: 對應的參數數
c)success : 成功時的回調方
d)fail:失敗時的回調方
典型的調用如下:
var success = function(data){};var fail = functio(data){};exec_asyn( "Contacts", "openContacts" , '{}', success, fail);
在這裡,AndroidHtml5是在Javascript中定義的一個對象,它提供了訪問Android原生環境的方法,以及回調的隊列函數。它的定義如下:
var AndroidHtml5 = { idCounter : 0, // 參數序列計數器 OUTPUT_RESULTS : {}, // 輸出的結果 CALLBACK_SUCCESS : {}, // 輸出的結果成功時調用的方法 CALLBACK_FAIL : {}, // 輸出的結果失敗時調用的方法 callNative : function (cmd, args, success, fail) { var key = "ID_" + (++ this.idCounter); window.nintf.setCmds(cmd, key); window.nintf.setArgs(args, key); if (typeof success != 'undefined'){ AndroidHtml5.CALLBACK_SUCCESS[key] = success; } else { AndroidHtml5.CALLBACK_SUCCESS[key] = function (result){}; } if (typeof fail != 'undefined'){ AndroidHtml5.CALLBACK_FAIL[key] = fail; } else { AndroidHtml5.CALLBACK_FAIL[key] = function (result){}; } //下面會定義一個Iframe,Iframe會去載入我們自訂的url,以androidhtml:開頭 var iframe = document.createElement("IFRAME" ); iframe.setAttribute( "src" , "androidhtml://ready?id=" + key); document.documentElement.appendChild(iframe); iframe.parentNode.removeChild(iframe); iframe = null ; return this .OUTPUT_RESULTS[key]; }, callBackJs : function (result,key) { this .OUTPUT_RESULTS[key] = result; var obj = JSON.parse(result); var message = obj.message; var status = obj.status; if (status == 0) { if (typeof this.CALLBACK_SUCCESS[key] != "undefined"){ setTimeout( "AndroidHtml5.CALLBACK_SUCCESS['" +key+"']('" + message + "')", 0); } } else { if (typeof this.CALLBACK_FAIL != "undefined") { setTimeout( "AndroidHtml5.CALLBACK_FAIL['" +key+"']('" + message + "')" , 0); } } }};
在AndroidHtml5中,有幾個地方我們需要注意的。
a)大家還記得我們在WebView初始化時設定的AppJavascriptInterface嗎?當時自訂的名稱就是"nintf",而在此時,在javascript中,我們就可以直接來運用這個對象所有的方法。
window.nintf.setCmds(cmd, key);window.nintf.setArgs(args, key);
我們也看一下這個AppJavascriptInterface中的方法,如下:
public class AppJavascriptInterface implements java.io.Serializable { private static Hashtable<String,String> CMDS = new Hashtable<String,String>(); private static Hashtable<String,String> ARGS = new Hashtable<String,String>(); @JavascriptInterface public void setCmds(String cmds, String id) { CMDS .put(id, cmds); } @JavascriptInterface public void setArgs(String args, String id) { ARGS .put(id, args); } public static String getCmdOnce(String id) { String result = CMDS .get(id); CMDS .remove(id); return result; } public static String getArgOnce(String id) { String result = ARGS .get(id); ARGS .remove(id); return result; }}
這個類是簡潔而不簡單,通過在Javascript中調用類中的set方法,將對應的cmd和args參數給儲存起來,目的是為了儲存非同步請求中多次的命令和操作,然後在Android原生環境中再取出來。
b)第二步呢,也是最重要的一步,會建立一個Iframe,在Iframe中申明一個url,而且是以androidhtml: 開頭的。
在上面我們提過,WebView在初始化的時候,會設定一個WebViewClient,這個類的主要作用就是,當在html頁面中發生url載入的時候,我們可以攔截這個載入事件,進行處理,重寫這次載入事件。
而我們正好是利用了這一點,利用一個Iframe來觸發一次Url的攔截事件。
我們來看一下WebViewClient中是如何?這個非同步請求的實現的。
class WebServerViewClient extends WebViewClient { Handler myHandler = new Handler() { ... }; @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url != null && url.startsWith( "androidhtml")) { String id = url.substring(url.indexOf( "id=" ) + 3); JSONObject cmd = null ; JSONObject arg = null ; try { String cmds = AppJavascriptInterface.getCmdOnce(id); String args = AppJavascriptInterface.getArgOnce(id); cmd = new JSONObject(cmds); arg = new JSONObject(args); } catch (JSONException e1) { e1.printStackTrace(); return false ; } //另起線程處理請求 try { AsynServiceHandler asyn = new AsynServiceHandlerImpl(); asyn.setKey(id); asyn.setService(cmd.getString( "service" )); asyn.setAction(cmd.getString( "action" )); asyn.setArgs(arg); asyn.setWebView( mWebView); asyn.setMessageHandler( myHandler ); Thread thread = new Thread(asyn, "asyn_" + (threadIdCounter ++)); thread.start(); } catch (Exception e) { e.printStackTrace(); return false; } return true ; } //如果url不是以Androidhtml開頭的,則由WebView繼續去處理。 view.loadUrl(url); return true ; } }我們可以看到,在這方法中,首先只有以androidhtml開頭的url才會被攔截處理,而其他的url則還是由WebView進行處理。
而通過AppJavascriptInterface,我們將在Javascript中儲存的cmds和args等資料都拿出來了,並由AsynServiceHandler新啟一個線程去處理。
我們再來看看AsynServiceHandlerImpl是怎麼實現的,
public class AsynServiceHandlerImpl implements AsynServiceHandler { @Override public void run() { try { final String responseBody = PluginManager.getInstance().exec(service, action,args); handler.post( new Runnable() { public void run() { webView .loadUrl( "javascript:AndroidHtml5.callBackJs('"+responseBody+ "','" +key +"')" ); } }); } catch (PluginNotFoundException e) { e.printStackTrace(); } }可以看到,當調用PluginManager操作完對應的命令和資料之後,會通過WebView的loadUrl方法,去執行AndroidHtml5的callBackJs方法。
通過key值,我們就可以在AndroidHtml5中的callBackJs方法中找回到對應的回調方法,進行處理。
因此,通過一次Iframe的構建,載入以androidhtml開頭的url,再利用WebView的WebViewClient介面對象,我們就能夠在Html頁面中和Android原生環境進行非同步互動了。
在這一篇文章中,我們幾處地方講到了PluginManager這個類,這是一個管理HTML和Android原生環境互動介面的類。
因為如果把所有的邏輯都放在WebViewClient或者WebChromeClient這兩個都來處理,這是不合理的,亂,複雜,看不懂。
所以我們需要把邏輯實現跟互動給分開來,這個機制才顯得漂亮,實用,易操作。