大家都知道,通过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这两个都来处理,这是不合理的,乱,复杂,看不懂。

所以我们需要把逻辑实现跟交互给分开来,这个机制才显得漂亮,实用,易操作。


更多相关文章

  1. Android搜索控件的基本使用方法
  2. Android零基础入门第34节:Android中基于监听的事件处理
  3. Android官方离线文档(API文档)打开速度慢的解决方法
  4. 搭建自己的android 开发环境 (eclipse + ADT)
  5. Android IPC机制(三)在Android Studio中使用AIDL实现跨进程方法调
  6. android实现观察者模式的几种方法
  7. Android Studio怎样提示函数使用方法
  8. Android 事件全局监听(二)需要root权限 ,使用getevent监听Android输

随机推荐

  1. 教你怎么样快速通过XSL转换XML文件
  2. 基于XML的购物车的实例代码详情
  3. linq to xml操作XML的方法
  4. 使用XMLHTTP发送超长XML表单数据的详情
  5. XML数据库中几个容易混淆的概念详细介绍
  6. 使用正则表达式进行xml数据验证的代码实
  7. 用XSL翻译Web服务应用程序
  8. 用XML和SQL 2000来管理存储过程调用
  9. 对XML数据使用XMLConvert
  10. 详细介绍把数据转换成XML格式的好处