相比于 Native App 和 Web App,Hybrid App 憑借其迭代靈活环戈、控制自如闷板、多端同步的優(yōu)勢在應用市場上越發(fā)顯得優(yōu)勝,主要得力于院塞,其將變更頻繁的部分產(chǎn)品功能使用 H5 開發(fā)并在客戶端中借助 WebView 控件嵌入應用當中遮晚。所以,開發(fā)中我們總會遇到原生 Java 代碼與網(wǎng)頁中的 Js 代碼之間相互調(diào)用從而產(chǎn)生的交互問題拦止。
Java 與 Js 彼此調(diào)用的前提是設置 WebView 支持 JavaScript 功能:
mWebView.getSettings().setJavaScriptEnabled(true);
Java 調(diào)用 Js
第一步县遣,在網(wǎng)頁中使用 Js 定義提供給 Java 訪問的方法糜颠,就像普通方法定義一樣,如:
<script type="text/javascript">
? ? function javaCallJs(message){
? ? ? ? alert(message);
? ? }
</script>
第二步萧求,在 Java 代碼中按照 "javascript:XXX" 的 Url 格式使用 WebView 加載訪問即可:
mWebView.loadUrl("javascript:javaCallJs(" + "'Message From Java'" + ")");
注意:String 類型的參數(shù)需要使用單引號 “'” 包裹括蝠,數(shù)組類型的參數(shù)則不用,如:javascript:javaCallJs([01, 02, 03])饭聚,其他復雜類型的參數(shù)可以轉(zhuǎn)換為 Json 字符串的形式傳遞忌警。
Js 調(diào)用 Java
第一步,在 Java 對象中定義 Js 訪問的方法秒梳,如:
@JavascriptInterface
public void jsCallJava(String message){
? ? Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
注意事項:提供給 Js 訪問的屬性和方法必須定義為 public 類型法绵,并且添加注解 @JavascriptInterface。在 API 17 及更高版本的系統(tǒng)中酪碘,任何暴露給 Js 訪問的 Java 接口都需要添加這個注解朋譬,否則會報異常:Uncaught TypeError: Object [object Object] has no method 'XXX'。系統(tǒng)這種做法也是為了降低應用的安全隱患兴垦,因為在之前的版本中徙赢,Js 可以通過反射的方式訪問注入 WebView 中的 Java 對象的 public 類型 field 和 method,從而隨意修改宿主程序探越。
第二步狡赐,將提供給 Js 訪問的接口內(nèi)容所屬的 Java 對象注入 WebView 中:
mWebView.addJavascriptInterface(MainActivity.this, "main");
addJavascriptInterface(Object object, String name) 參數(shù)說明:object 表示 Js 訪問的接口內(nèi)容所在的 Java 對象;name 表示 Js 調(diào)用 Java 代碼時的接口名稱钦幔,與 Js 中的調(diào)用保持一致即可枕屉。
第三步,Js 按照指定的接口名訪問 Java 代碼鲤氢,有如下兩種寫法:
<button type="button" onClick="javascript:main.jsCallJava('Message From Js')" >Js Call Java</button>
<!--<button type="button" onClick="window.main.jsCallJava('Message From Js')" >Js Call Java</button>-->
這里簡單提供一個可供測試的 Html 網(wǎng)頁和 Activity 代碼:
test.html:
<html>
? ? <head>?
? ? ? ? <meta http-equiv="Content-Type"? content="text/html;charset=UTF-8">
? ? ? ? <script type="text/javascript">
? ? ? ? ? ? function javaCallJs(message){
? ? ? ? ? ? ? ? alert(message);
? ? ? ? ? ? }
? ? ? ? </script>
? ? </head>?
? ? <body>
? ? ? ? <button type="button" onClick="window.main.jsCallJava('Message From Js')" >Js Call Java</button>
? ? </body>?
</html>
MainActivity.java:
public class MainActivity extends AppCompatActivity {
? ? private WebView mWebView;
? ? @Override
? ? protected void onCreate(Bundle savedInstanceState) {
? ? ? ? super.onCreate(savedInstanceState);
? ? ? ? setContentView(R.layout.activity_main);
? ? ? ? Toolbar mToolbarTb = (Toolbar) findViewById(R.id.tb_toolbar);
? ? ? ? setSupportActionBar(mToolbarTb);
? ? ? ? mWebView = (WebView) findViewById(R.id.webview);
? ? ? ? mWebView.getSettings().setJavaScriptEnabled(true);
? ? ? ? mWebView.loadUrl("file:///android_asset/test.html");
? ? ? ? mWebView.addJavascriptInterface(MainActivity.this, "main");
? ? ? ? mWebView.setWebChromeClient(new WebChromeClient() {
? ? ? ? ? ? @Override
? ? ? ? ? ? public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
? ? ? ? ? ? ? ? return super.onJsAlert(view, url, message, result);
? ? ? ? ? ? }
? ? ? ? });
? ? }
? ? public void javaCallJs(View v){
? ? ? ? mWebView.loadUrl("javascript:javaCallJs(" + "'Message From Java'" + ")");
? ? }
? ? @JavascriptInterface
? ? public void jsCallJava(String message){
? ? ? ? Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
? ? }
? ? @Override
? ? public boolean onCreateOptionsMenu(Menu menu) {
? ? ? ? getMenuInflater().inflate(R.menu.search, menu);
? ? ? ? return super.onCreateOptionsMenu(menu);
? ? }
}
注意:無論是 Java 調(diào)用 Js 還是 Js 調(diào)用 Java搀擂,只能通過參數(shù)傳遞數(shù)據(jù),而無法獲取彼此方法的返回值卷玉!解決方案就是額外添加一層回調(diào)來達到這個目的哨颂。比如 Java 調(diào)用 Js 的方法,Js 計算結(jié)束所得結(jié)果不能通過 return 語句返回給 Java 調(diào)用者相种,而是再回調(diào) Java 的另一個方法威恼,通過傳參的形式傳遞給 Java。
注意事項
1.使用 loadUrl() 方法實現(xiàn) Java 調(diào)用 Js 功能時蚂子,必須放置在主線程中沃测,否則會發(fā)生崩潰異常。比如修改上面的代碼:
new Thread(new Runnable() {
? ? @Override
? ? public void run() {
? ? ? ? mWebView.loadUrl("javascript:javaCallJs(" + "'Message From Java'" + ")");
? ? }
}).start();
運行時會得到如下 logcat 異常信息:
java.lang.RuntimeException: java.lang.Throwable: A WebView method was called on thread 'Thread-18022'. All WebView methods must be called on the same thread.
如果真的在子線程中遇到調(diào)用 Js 的功能食茎,也要將其轉(zhuǎn)換到主線程中去:
mWebView.post(new Runnable() {
? ? @Override
? ? public void run() {
? ? ? ? mWebView.loadUrl("javascript:javaCallJs(" + "'Message From Java'" + ")");
? ? }
});
2.Js 調(diào)用 Java 方法時蒂破,不是在主線程 (Thread Name:main) 中運行的,而是在一個名為 JavaBridge 的線程中執(zhí)行的别渔,通過如下代碼可以測試:
@JavascriptInterface
? ? public void jsCallJava(String message){
? ? ? ? Log.i("thread", Thread.currentThread().getName());
? ? ? ? Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
? ? }
所以這里需要注意的是附迷,當 Js 調(diào)用 Java 時惧互,如果需要 Java 繼續(xù)回調(diào) Js,千萬別在 JavascriptInterface 方法體中直接執(zhí)行 loadUrl() 方法喇伯,而是像前面一樣進行線程切換操作喊儡。
3.代碼混淆時,記得保持 JavascriptInterface 內(nèi)容稻据,在 proguard 文件中添加如下類似規(guī)則 (有關類名按需修改):
keepattributes *Annotation*
keepattributes JavascriptInterface
-keep public class com.mypackage.MyClass$MyJavaScriptInterface
-keep public class * implements com.mypackage.MyClass$MyJavaScriptInterface
-keepclassmembers class com.mypackage.MyClass$MyJavaScriptInterface {
? ? <methods>;
}
Url 攔截
除了上面這種 Java 與 Js 互調(diào)方法的方式艾猜,還可以利用 WebView 攔截 Url 的方式實現(xiàn)原生應用與 H5 之間的交互動作。通過 WebViewClient 提供的接口攔截網(wǎng)頁內(nèi)諸如二級跳轉(zhuǎn)的 Url 鏈接捻悯,便可以進行業(yè)務邏輯上的判斷處理匆赃、Url 參數(shù)傳遞等功能,如:
mWebView.setWebViewClient(new WebViewClient(){
? ? @Override
? ? public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
? ? ? ? // request.getUrl()
? ? ? ? return super.shouldOverrideUrlLoading(view, request);
? ? }
});
注意:過去常用的 shouldOverrideUrlLoading(WebView view, String url) 方法已經(jīng)被廢棄今缚。
參考使用
通過 Java 與 Js 之間的交互可以做很多事情算柳,比如獲取網(wǎng)頁中的圖片,利用原生控件予以展示姓言,類似響應微信公眾號文章中的圖片點擊事件瞬项。參考代碼如下:
public class MainActivity extends AppCompatActivity {
? ? private WebView mWebView;
? ? @Override
? ? protected void onCreate(Bundle savedInstanceState) {
? ? ? ? super.onCreate(savedInstanceState);
? ? ? ? setContentView(R.layout.activity_main);
? ? ? ? mWebView = (WebView) findViewById(R.id.webview);
? ? ? ? mWebView.getSettings().setJavaScriptEnabled(true);
? ? ? ? mWebView.loadUrl("https://www.taobao.com/");
? ? ? ? mWebView.addJavascriptInterface(new MyJavascriptInterface(), "imageClick");
? ? ? ? mWebView.setWebViewClient(new MyWebViewClient());
? ? }
? ? /**
? ? * 遍歷 <img> 標簽, 添加圖片點擊事件, 將圖片 Url 地址回調(diào)給 Java 方法
? ? */
? ? private void addImageClickListner() {
? ? ? ? mWebView.loadUrl("javascript:(function(){" +
? ? ? ? ? ? ? ? "var objs = document.getElementsByTagName(\"img\"); " +
? ? ? ? ? ? ? ? "for(var i=0;i<objs.length;i++)? " +
? ? ? ? ? ? ? ? "{"
? ? ? ? ? ? ? ? + "? ? objs[i].onclick=function()? " +
? ? ? ? ? ? ? ? "? ? {? "
? ? ? ? ? ? ? ? + "? ? ? ? window.imageClick.openImage(this.src);? " +
? ? ? ? ? ? ? ? "? ? }? " +
? ? ? ? ? ? ? ? "}" +
? ? ? ? ? ? ? ? "})()");
? ? }
? ? public class MyJavascriptInterface {
? ? ? ? public MyJavascriptInterface() {
? ? ? ? }
? ? ? ? @android.webkit.JavascriptInterface
? ? ? ? public void openImage(String imageUrl) {
? ? ? ? ? ? Log.i("imageUrl", imageUrl);
? ? ? ? ? ? // TODO 獲取圖片地址后, 通過原生控件 ImageView 展示, 添加縮放、保存等功能
? ? ? ? }
? ? }
? ? private class MyWebViewClient extends WebViewClient {
? ? ? ? @Override
? ? ? ? public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
? ? ? ? ? ? return super.shouldOverrideUrlLoading(view, request);
? ? ? ? }
? ? ? ? @Override
? ? ? ? public void onPageFinished(WebView view, String url) {
? ? ? ? ? ? super.onPageFinished(view, url);
? ? ? ? ? ? addImageClickListner();
? ? ? ? }
? ? }
}