編輯:關於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 Hashtable CMDS = 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 on Android:資源文件系統qrc與assets
使用 Qt 為 Android 開發應用時,有時我們的應用會攜帶一些資源文件,如 png 、 jpg 等,也可能有一些配置文件,如 xml 等,這些文件放在哪裡呢?
Android新特性介紹,ConstraintLayout完全解析
今天給大家帶來2017年的第一篇文章,這裡先祝大家新年好。本篇文章的主題是ConstraintLayout。其實ConstraintLayout是AndroidStudi
Android進階——ViewPager詳解之初識ViewPager(一)
引言最近在工作中由於需要客制化系統的關系,接觸到了很多ViewPager相關的UI,發現很多底層原生的界面也還是依然采用ViewPager+Fragment的布局方式,事
Android開發--仿微信語音對講錄音
自微信出現以來取得了很好的成績,語音對講的實現更加方便了人與人之間的交流。今天來實踐一下微信的語音對講的錄音實現,這個也比較容易實現。在此,我將該按鈕封裝成為一個控件,並