越来越发现把自己的思路理清楚并且能够给别人讲明白是件很困难的事情,技术博客不等于小说、散文,不能天马行空,思维必须严谨,思路必须清晰。
写技术博客要随时审视自己的思路是不是还连贯,要不断返回去看代码,再从代码理清思路。有时还要查找资料来把某个点解释清楚,当然其中会有很多自己不太清楚的地方,所以也希望博友能一起交流、讨论,给指出不正确的地方,博客确实是一个互相进步的很好的手段。
好了,不多说了,进入正题。
昨天讲解了DroidGap类,DroidGap继承Activity,在DroidGap的init()初始化方法中设置了WebView,今天主要讲解CordovaWebView类以及CordovaWebviewClient类和CordovaChromeClient类。
CordovaWebView类
public class CordovaWebView extends WebView {...}
CordovaWebView类继承WebView类,在上一篇博文中已经对WebView类做了简要的介绍。PhoneGap针对不同平台的WebView做了扩展和封装,使WebView这个组件变成可访问设备本地API的强大浏览器,所以开发人员在PhoneGap框架下可通过JavaScript访问设备本地API。
可以说,CordovaWebView是整个PhoneGap的核心组件。
1 /** 2 * Constructor. 3 * 4 * @param context 5 */ 6 public CordovaWebView(Context context) { 7 super(context); 8 if (CordovaInterface.class.isInstance(context)) 9 { 10 this.cordova = (CordovaInterface) context; 11 } 12 else 13 { 14 Log.d(TAG, "Your activity must implement CordovaInterface to work"); 15 } 16 this.loadConfiguration(); 17 this.setup(); 18 }
CordovaWebView类的构造函数中,需要传入Context,并且Context必须是CordovaInterface的实现类(这里需要特别注意)。构造函数里调用loadConfiguration()和Setup()方法。在CordovaWebView类的构造函数重载方法中,还有setWebChromeClient和setWebViewClient对CordovaWebView的设置,原因在上篇最后也有讲到,这两个类的具体讲解在下面。
1 /** 2 * Load Cordova configuration from res/xml/cordova.xml. 3 * Approved list of URLs that can be loaded into DroidGap 4 * <access origin="http://server regexp" subdomains="true" /> 5 * Log level: ERROR, WARN, INFO, DEBUG, VERBOSE (default=ERROR) 6 * <log level="DEBUG" /> 7 */ 8 private void loadConfiguration() { 9 int id = getResources().getIdentifier("cordova", "xml", this.cordova.getActivity().getPackageName()); 10 // int id = getResources().getIdentifier("cordova", "xml", this.cordova.getPackageName()); 11 if (id == 0) { 12 LOG.i("CordovaLog", "cordova.xml missing. Ignoring..."); 13 return; 14 } 15 XmlResourceParser xml = getResources().getXml(id); 16 int eventType = -1; 17 while (eventType != XmlResourceParser.END_DOCUMENT) { 18 if (eventType == XmlResourceParser.START_TAG) { 19 String strNode = xml.getName(); 20 if (strNode.equals("access")) { 21 String origin = xml.getAttributeValue(null, "origin"); 22 String subdomains = xml.getAttributeValue(null, "subdomains"); 23 if (origin != null) { 24 this.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0)); 25 } 26 } 27 else if (strNode.equals("log")) { 28 String level = xml.getAttributeValue(null, "level"); 29 LOG.i("CordovaLog", "Found log level %s", level); 30 if (level != null) { 31 LOG.setLogLevel(level); 32 } 33 } 34 else if (strNode.equals("preference")) { 35 String name = xml.getAttributeValue(null, "name"); 36 String value = xml.getAttributeValue(null, "value"); 37 38 LOG.i("CordovaLog", "Found preference for %s=%s", name, value); 39 Log.d("CordovaLog", "Found preference for " + name + "=" + value); 40 41 // Save preferences in Intent 42 this.cordova.getActivity().getIntent().putExtra(name, value); 43 } 44 } 45 try { 46 eventType = xml.next(); 47 } catch (XmlPullParserException e) { 48 e.printStackTrace(); 49 } catch (IOException e) { 50 e.printStackTrace(); 51 } 52 } 53 54 // Init preferences 55 if ("true".equals(this.getProperty("useBrowserHistory", "false"))) { 56 this.useBrowserHistory = true; 57 } 58 else { 59 this.useBrowserHistory = false; 60 } 61 62 if ("true".equals(this.getProperty("fullscreen", "false"))) { 63 this.cordova.getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); 64 this.cordova.getActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); 65 } 66 }
loadConfiguration()的作用是从res/xml/cordova.xml文件中加载Cordova配置信息,包括允许加载本地网页等,xml文件没几行,不细说。
1 /** 2 * Initialize webview. 3 */ 4 @SuppressWarnings("deprecation") 5 private void setup() { 6 7 this.setInitialScale(0); 8 this.setVerticalScrollBarEnabled(false); 9 this.requestFocusFromTouch(); 10 11 // Enable JavaScript 12 WebSettings settings = this.getSettings(); 13 settings.setJavaScriptEnabled(true); 14 settings.setJavaScriptCanOpenWindowsAutomatically(true); 15 settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL); 16 17 //Set the nav dump for HTC 18 settings.setNavDump(true); 19 20 // Enable database 21 settings.setDatabaseEnabled(true); 22 String databasePath = this.cordova.getActivity().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath(); 23 settings.setDatabasePath(databasePath); 24 25 // Enable DOM storage 26 settings.setDomStorageEnabled(true); 27 28 // Enable built-in geolocation 29 settings.setGeolocationEnabled(true); 30 31 //Start up the plugin manager 32 try { 33 this.pluginManager = new PluginManager(this, this.cordova); 34 } catch (Exception e) { 35 // TODO Auto-generated catch block 36 e.printStackTrace(); 37 } 38 }
setup()方法初始化WebView配置,包括设置WebView初始化视图大小、是否允许垂直滚动条、触摸焦点信息。
因为PhoneGap的强大之处在于可以在Web端直接调用底层API,包括照相机、指南针、GPS等设备,其实现这些功能是通过JavaScript与底层交互实现的,所以在接下来设置settings.setJavaScriptEnabled(true)也就理所当然的事情了。
然后设置了数据库database、DOM storage和地理位置应用geolocation等信息,这里也不细讲。
需要着重提到的一点是最后PhoneGap插件的初始化。没错,PhoneGap插件的初始化是在这里进行的。PhoneGap插件管理机制是PhoneGap实现跨平台的基础。pluginManager类在后面也会讲到。
在上篇中讲到了DroidGap类loadUrl()方法,我们可以把它看成是整个PhoneGap应用的入口函数。实际DroidGap类的loadUrl()方法调用了CordovaWebView类的loadUrl()方法:
1 /** 2 * Load the url into the webview. 3 * 4 * @param url 5 */ 6 @Override 7 public void loadUrl(String url) { 8 if (url.equals("about:blank") || url.startsWith("javascript:")) { 9 this.loadUrlNow(url); 10 } 11 else { 12 13 String initUrl = this.getProperty("url", null); 14 15 // If first page of app, then set URL to load to be the one passed in 16 if (initUrl == null || (this.urls.size() > 0)) { 17 this.loadUrlIntoView(url); 18 } 19 // Otherwise use the URL specified in the activity's extras bundle 20 else { 21 this.loadUrlIntoView(initUrl); 22 } 23 } 24 }
代码不多,也比较好理解,自己看吧,不细讲了。O(∩_∩)O~
1 /** 2 * Load the url into the webview. 3 * 4 * @param url 5 */ 6 public void loadUrlIntoView(final String url) { 7 LOG.d(TAG, ">>> loadUrl(" + url + ")"); 8 9 this.url = url; 10 if (this.baseUrl == null) { 11 int i = url.lastIndexOf('/'); 12 if (i > 0) { 13 this.baseUrl = url.substring(0, i + 1); 14 } 15 else { 16 this.baseUrl = this.url + "/"; 17 } 18 19 // this.pluginManager.init(); 20 21 if (!this.useBrowserHistory) { 22 this.urls.push(url); 23 } 24 } 25 26 // Create a timeout timer for loadUrl 27 final CordovaWebView me = this; 28 final int currentLoadUrlTimeout = me.loadUrlTimeout; 29 final int loadUrlTimeoutValue = Integer.parseInt(this.getProperty("loadUrlTimeoutValue", "20000")); 30 31 // Timeout error method 32 final Runnable loadError = new Runnable() { 33 public void run() { 34 me.stopLoading(); 35 LOG.e(TAG, "CordovaWebView: TIMEOUT ERROR!"); 36 if (viewClient != null) { 37 viewClient.onReceivedError(me, -6, "The connection to the server was unsuccessful.", url); 38 } 39 } 40 }; 41 42 // Timeout timer method 43 final Runnable timeoutCheck = new Runnable() { 44 public void run() { 45 try { 46 synchronized (this) { 47 wait(loadUrlTimeoutValue); 48 } 49 } catch (InterruptedException e) { 50 e.printStackTrace(); 51 } 52 53 // If timeout, then stop loading and handle error 54 if (me.loadUrlTimeout == currentLoadUrlTimeout) { 55 me.cordova.getActivity().runOnUiThread(loadError); 56 } 57 } 58 }; 59 60 // Load url 61 this.cordova.getActivity().runOnUiThread(new Runnable() { 62 public void run() { 63 Thread thread = new Thread(timeoutCheck); 64 thread.start(); 65 me.loadUrlNow(url); 66 } 67 }); 68 }
这里需要细讲一下。Android WebView里并没有对加载超时的处理,PhoneGap自己实现了加载超时处理的方法。虽然看不看这里并不会影响对PhoneGap整个机制的理解,但拿出来讲一下对以后可能也会很有用处。
从代码的第26行注释看起,CordovaWebViewClient类有一个属性LoadUrlTimeout,这里不要被其定义为int类型迷惑,实际这个变量完全可以是boolean布尔值,因为它只有两个值:0和1。在页面开始加载时currentLoadUrlTimeout初始化为me.loadUrlTimeout(这个值初始化为0),在页面加载完成(注意:这里CordovaWebViewClient派上用场了),即CordovaWebViewClient类里OnpageFinished()方法中appView.LoadUrlTimeout被置成1。定义了loadError和runOnUiThread两个线程。然后起了一个在Timeout之后执行的线程,具体执行那个线程就看页面是否加载完成,即OnpageFinished()方法中appView.LoadUrlTimeout是否被置成了1。
CordovaWebViewClient类
CordovaWebViewClient类主要处理各种通知、请求事件的,重写了WebView类的一些方法,这里主要讲解其中onPageStarted()和onPageFinished()两个方法。
1 /** 2 * Notify the host application that a page has started loading. 3 * This method is called once for each main frame load so a page with iframes or framesets will call onPageStarted 4 * one time for the main frame. This also means that onPageStarted will not be called when the contents of an 5 * embedded frame changes, i.e. clicking a link whose target is an iframe. 6 * 7 * @param view The webview initiating the callback. 8 * @param url The url of the page. 9 */ 10 @Override 11 public void onPageStarted(WebView view, String url, Bitmap favicon) { 12 // Clear history so history.back() doesn't do anything. 13 // So we can reinit() native side CallbackServer & PluginManager. 14 if (!this.appView.useBrowserHistory) { 15 view.clearHistory(); 16 this.doClearHistory = true; 17 } 18 19 // Create callback server and plugin manager 20 if (this.appView.callbackServer == null) { 21 this.appView.callbackServer = new CallbackServer(); 22 this.appView.callbackServer.init(url); 23 } 24 else { 25 this.appView.callbackServer.reinit(url); 26 } 27 28 // Broadcast message that page has loaded 29 this.appView.postMessage("onPageStarted", url); 30 }
从文档注释我们可以看出,onPageStarted方法用于通知主应用程序Web页面开始加载了。其中在页面加载时比较重要的一步是创建CallbackServer对象并初始化。(CallbackServer类可是实现插件异步执行的重头戏啊,好紧张啊,写了这么久CallbackServer终于出现了啊o(╯□╰)o)
1 /** 2 * Notify the host application that a page has finished loading. 3 * This method is called only for main frame. When onPageFinished() is called, the rendering picture may not be updated yet. 4 * 5 * 6 * @param view The webview initiating the callback. 7 * @param url The url of the page. 8 */ 9 @Override 10 public void onPageFinished(WebView view, String url) { 11 super.onPageFinished(view, url); 12 LOG.d(TAG, "onPageFinished(" + url + ")"); 13 14 /** 15 * Because of a timing issue we need to clear this history in onPageFinished as well as 16 * onPageStarted. However we only want to do this if the doClearHistory boolean is set to 17 * true. You see when you load a url with a # in it which is common in jQuery applications 18 * onPageStared is not called. Clearing the history at that point would break jQuery apps. 19 */ 20 if (this.doClearHistory) { 21 view.clearHistory(); 22 this.doClearHistory = false; 23 } 24 25 // Clear timeout flag 26 this.appView.loadUrlTimeout++; 27 28 // Try firing the onNativeReady event in JS. If it fails because the JS is 29 // not loaded yet then just set a flag so that the onNativeReady can be fired 30 // from the JS side when the JS gets to that code. 31 if (!url.equals("about:blank")) { 32 this.appView.loadUrl("javascript:try{ cordova.require('cordova/channel').onNativeReady.fire();}catch(e){_nativeReady = true;}"); 33 this.appView.postMessage("onNativeReady", null); 34 } 35 36 // Broadcast message that page has loaded 37 this.appView.postMessage("onPageFinished", url); 38 39 // Make app visible after 2 sec in case there was a JS error and Cordova JS never initialized correctly 40 if (this.appView.getVisibility() == View.INVISIBLE) { 41 Thread t = new Thread(new Runnable() { 42 public void run() { 43 try { 44 Thread.sleep(2000); 45 cordova.getActivity().runOnUiThread(new Runnable() { 46 public void run() { 47 appView.postMessage("spinner", "stop"); 48 } 49 }); 50 } catch (InterruptedException e) { 51 } 52 } 53 }); 54 t.start(); 55 } 56 57 // Shutdown if blank loaded 58 if (url.equals("about:blank")) { 59 if (this.appView.callbackServer != null) { 60 this.appView.callbackServer.destroy(); 61 } 62 appView.postMessage("exit", null); 63 } 64 }
上文已经提到在onPageFinished方法中会执行this.appView.loadUrlTimeout++,定时器loadUrlTimeout的值被置为1,表明页面加载完成。如果没有执行到此,说明加载超时,在CordovaWebView类的loadUrlIntoView()方法中判断if(currentLoadUrlTimeout ==me.loadUrlTimeout),则执行loadError线程。
CordovaChromeClient类
WebChromeClient是辅助WebView处理Javascript的对话框,网站图标,网站title,加载进度等,一般的WebView可以不设置ChromeClient,比如你的WebView不需要处理JavaScript脚本。前面已经提到为什么PhoneGap要设置ChromeClient,不再多说。
引用某位大侠的话说,关于Java/JS互调,在android sdk文档中,也有用JsInterface和loadUrl做到交互的示例。PhoneGap并没有选择用JsInterface,而是使用拦截prompt这种hack做法。
对JavaScript消息的拦截是在onJsPrompt()方法中实现并处理的。
1 /** 2 * Tell the client to display a prompt dialog to the user. 3 * If the client returns true, WebView will assume that the client will 4 * handle the prompt dialog and call the appropriate JsPromptResult method. 5 * 6 * Since we are hacking prompts for our own purposes, we should not be using them for 7 * this purpose, perhaps we should hack console.log to do this instead! 8 * 9 * @param view 10 * @param url 11 * @param message 12 * @param defaultValue 13 * @param result 14 */ 15 @Override 16 public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 17 18 // Security check to make sure any requests are coming from the page initially 19 // loaded in webview and not another loaded in an iframe. 20 boolean reqOk = false; 21 if (url.startsWith("file://") || url.indexOf(this.appView.baseUrl) == 0 || this.appView.isUrlWhiteListed(url)) { 22 reqOk = true; 23 } 24 25 // Calling PluginManager.exec() to call a native service using 26 // prompt(this.stringify(args), "gap:"+this.stringify([service, action, callbackId, true])); 27 if (reqOk && defaultValue != null && defaultValue.length() > 3 && defaultValue.substring(0, 4).equals("gap:")) { 28 JSONArray array; 29 try { 30 array = new JSONArray(defaultValue.substring(4)); 31 String service = array.getString(0); 32 String action = array.getString(1); 33 String callbackId = array.getString(2); 34 boolean async = array.getBoolean(3); 35 String r = this.appView.pluginManager.exec(service, action, callbackId, message, async); 36 result.confirm(r); 37 } catch (JSONException e) { 38 e.printStackTrace(); 39 } 40 } 41 42 // Polling for JavaScript messages 43 else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) { 44 String r = this.appView.callbackServer.getJavascript(); 45 result.confirm(r); 46 } 47 48 // Do NO-OP so older code doesn't display dialog 49 else if (defaultValue != null && defaultValue.equals("gap_init:")) { 50 result.confirm("OK"); 51 } 52 53 // Calling into CallbackServer 54 else if (reqOk && defaultValue != null && defaultValue.equals("gap_callbackServer:")) { 55 String r = ""; 56 if (message.equals("usePolling")) { 57 r = "" + this.appView.callbackServer.usePolling(); 58 } 59 else if (message.equals("restartServer")) { 60 this.appView.callbackServer.restartServer(); 61 } 62 else if (message.equals("getPort")) { 63 r = Integer.toString(this.appView.callbackServer.getPort()); 64 } 65 else if (message.equals("getToken")) { 66 r = this.appView.callbackServer.getToken(); 67 } 68 result.confirm(r); 69 } 70 71 // Show dialog 72 else { 73 final JsPromptResult res = result; 74 AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity()); 75 dlg.setMessage(message); 76 final EditText input = new EditText(this.cordova.getActivity()); 77 if (defaultValue != null) { 78 input.setText(defaultValue); 79 } 80 dlg.setView(input); 81 dlg.setCancelable(false); 82 dlg.setPositiveButton(android.R.string.ok, 83 new DialogInterface.OnClickListener() { 84 public void onClick(DialogInterface dialog, int which) { 85 String usertext = input.getText().toString(); 86 res.confirm(usertext); 87 } 88 }); 89 dlg.setNegativeButton(android.R.string.cancel, 90 new DialogInterface.OnClickListener() { 91 public void onClick(DialogInterface dialog, int which) { 92 res.cancel(); 93 } 94 }); 95 dlg.create(); 96 dlg.show(); 97 } 98 return true; 99 }
onJsPrompt到底拦截了哪些信息呢?
从传入参数来说,比较重要的有两个:message和defaultValue。
message字符串存放了插件的应用信息,如Camera插件的图片质量、图片是否可编辑,图片返回类型等。
defaultValue字符串存放了插件信息:service(如Camera)、action(如getPicture())、callbackId、async等。
拦截到这些信息后就会调用this.appView.pluginManager.exec(service, action, callbackId, message, async)去执行。
当然onJsPrompt不只实现插件调用信息,还有JavaScript polling轮询信息、CallbackServer调用信息等处理。
呃,今天写到这里吧,欢迎大家批评指正,也可以相互交流。