編輯:關於Android編程
大家都知道,通過WebView,我們可以在Android客戶端,用Web開發的方式來開發我們的應用。
如果一個應用就是單純一個WebView,所有的邏輯都只需要在網頁上交互的話,那我們其實就只需要通過html和javascript來跟服務器交互就可以了。
但是很多情況下,我們的應用不是單純一個WebView就可以了,有可能會需要運用到Android本身的應用,比如拍照,就需要調用Android本身的照像機等,要產生震動,在需要運用到手機特性的一些場景下,肯定需要這麼一套機制在javascript和Android之間互相通信,包括同步和異步的方式,而這套機制就是本文中我想要介紹的。
一步一步來,我們先從最簡單的地方講起:
1)需要一個WebView去展現我們的頁面,首先定義一個布局,非常簡單,就是一個WebView,如下: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" );
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" );
將我們自定義的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));
在這裡,我們調用了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; ... } }
而當返回false的時候,則此事件會繼續傳遞給WebView,由WebView來處理。
由於我們這裡是要利用這個Prompt方法,來實現Javascript跟Android原生環境之間的同步訪問,所以我們在這裡會攔截這個事件進行處理。
在這裡,通過message和defaultValue,我們可以拿到javascript中prompt方法兩個參數的值,在這裡,它們是Json數據,在這裡進行解析之後,由PluginManager來進行處理,最後將結果返回給JsPromptResult的confirm方法中。
此結果就是javascript中prompt的返回值了。
而除了JsPrompt,還有類似Javascript中的Alert方法等,我們知道浏覽器彈出的Alert窗口跟我們手機應用中窗口風格樣式是很不一樣的,而作為一個應用,風格肯定要有一套統一的標准,所以一般情況下,我們也會攔截WebView中的Alert窗口,這個邏輯也同樣會是在這裡處理,如下:
@Override public 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 ; }
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);
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); } } } };
window.nintf.setCmds(cmd, key); window.nintf.setArgs(args, key);
public class AppJavascriptInterface implements java.io.Serializable { private static HashtableCMDS = new Hashtable (); private static Hashtable ARGS = new Hashtable (); @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; } }
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這兩個都來處理,這是不合理的,亂,復雜,看不懂。
所以我們需要把邏輯實現跟交互給分開來,這個機制才顯得漂亮,實用,易操作。
使用 Qt 為 Android 開發應用時,有時我們的應用會攜帶一些資源文件,如 png 、 jpg 等,也可能有一些配置文件,如 xml 等,這些文件放在哪裡呢?
今天給大家帶來2017年的第一篇文章,這裡先祝大家新年好。本篇文章的主題是ConstraintLayout。其實ConstraintLayout是AndroidStudi
引言最近在工作中由於需要客制化系統的關系,接觸到了很多ViewPager相關的UI,發現很多底層原生的界面也還是依然采用ViewPager+Fragment的布局方式,事
自微信出現以來取得了很好的成績,語音對講的實現更加方便了人與人之間的交流。今天來實踐一下微信的語音對講的錄音實現,這個也比較容易實現。在此,我將該按鈕封裝成為一個控件,並