@flyouting
        
        2014-03-20T08:01:47.000000Z
        字数 3700
        阅读 7878
    最近一段时间都在捣腾 Webview,对于之前的Jelly Bean中与KitKat中基于Chromium实现的不同,有点有意思的发现。如果你不知道我在说什么,可以理解,毕竟,除了一些很好的补充,API大多仍保持原状,我们可以从这两个地址了解更多相关资料,地址一,地址二。
免责声明:这篇文章我不打算描述所有不同的API,我只是想强调我经历的一些问题/解决方案/在使用WebView API开发应用程序时的发现。
我最近才发现这个类,它非常有用,在我目前所进行的项目中,用户可以通过点击链接或者列表项,去到他网站的不同页面,这些动作有不同的影响,在后者的情况下,应用程序必须明确地请求加载新的URL,否则就会再次加载相同的页面,但在前者中不用这样。
WebView.getHitTestResult()方法返回一个HitTestResult对象。对象中包含类型,url,这个类型用来表示被点击元素的类型,如果是个有HTTP src属性标签的HTML,类型会是WebView.HitTestResult.SRC_ANCHOR_TYPE。在Webview中,如果有一个不支持的元素被点击,4.4之前的api会返回一个null。4.4的api会返回一个非空对象。
Webview把所有的访问记录都存在一个称之为WebBackForwardList的数据结构中。可以通过WebView.copyBackForwardList()方法来获取列表检索,正如名字一样,它会返回一个Webview中保存的数据列表的副本。调用WebView.goBack()方法之后,应用需要检索之前页面的url,我们可以很简单的通过WebBackForwardList.getCurrentItem()方法得到。之前的实现是在返回当前页面,4.4的做法是返回之前的页面。
这一点有一点点模糊:如果一个页面包含一个这样的链接:<a href="javascript:;">...</a>,这种做法其实很常见如果你想把某个元素显示成链接,但是又没有链接任何资源时。4.4中当元素被触发,会触发WebViewClient.onPageFinished()回调。虽然不确定这里发生了什么,但这么做很好,因为如果你在页面完成了加载时注入javascript,它有可能注入两次,这会导致一些意想不到的事情发生。
Remote debugging:非常不错,如果你工作用到Webview很多,你会喜欢这个。链接里有很详细的细节,可以看看。 
WebView.evaluateJavascript():这个方法可以直接从注入的javascript得到一个回调结果。只是需要在方法调用时提供一个ValueCallback。4.4之前也可以实现类似效果,但是需要更多的样板。
因为处理不同的API实现而导致代码混乱的情况很常见,我发现避免这种情况的一个方式是创建一个适配器接口,隐藏了这些差异。如此,我最后实现的代码如下:
public interface IWebViewCompatibility {void injectWebView(WebView webView);void evaluateJavascript(String script, ValueCallbackAdapter callback);boolean httpLinkHit();String getPreviousPageUrl();// Adapter interface for legacy WebView APIpublic static interface ValueCallbackAdapter {void evaluateResult(String value);String javascriptInterfaceMethodName();}}
如果应用使用了什么注入框架,比如 Dagger,在注入之前,在适配器初始化时,我们需要添加一个if语句。
@Module(injects = WebViewFragment.class)public class WebViewModule {...@Provides @Singletonpublic IWebViewCompatibility provideWebViewCompatibility() {return SUPPORTS_KITKAT ? new ChromiumWebViewCompatibility(): new LegacyWebViewCompatibility();}}
我们看看之前的api是如何获取javascript的回调结果的。我们可以利用 JavascriptInterface 机制在页面添加helper接口,从这会接收到的javascript返回的结果(我的建议:在webview加载任何东西之前确保这一点,否则为了把接口加入页面,需要重新加载页面),现在我们实现传统的适配器接口:
public class LegacyWebViewCompatibility implements IWebViewCompatibility {private WebView webView;private ValueCallbackAdapter callback;@Override public void injectWebView(WebView webView) {this.webView = webView;this.webView.addJavascriptInterface(new LegacyCallbackInterfaceHelper(), NAME);}@Overridepublic void evaluateJavascript(final String script, ValueCallbackAdapter callback) {this.callback = callback;if (callback != null) {String js = String.format("javascript:{var res=%s;%s.%s(res);};void(0);", script, NAME,callback.javascriptInterfaceMethodName());webView.loadUrl(js);} else {webView.loadUrl("javascript:{" + script + "};void(0);"));}}...class LegacyCallbackInterfaceHelper {static final String NAME = "legacyAndroidCallbackInterfaceHelper";@JavascriptInterface @SuppressWarnings("unused") // Called from jspublic void jimdoDefined(String result) {((Activity) webView.getContext()).runOnUiThread(new Runnable() {@Override public void run() {LegacyWebViewCompatibilityDelegate.this.callback.evaluateResult(result);}});}}}
正如你所看到的,我们有很多方式来实现相同的结果。这里需要注意的是:回调应该运行在主线程——JavascriptInterface方法在WebView线程上运行,否则你会在log中得到一个警告。
从这点出发,我们可以用这点来检查是否javascript已经被注入,避免其被注入两次。
javascriptInjector.injectFunction(screen, "typeof jimdo === \'undefined\'",new WebViewCompatibilityDelegate.ValueCallbackAdapter() {@Override public void evaluateResult(String jimdoUndefined) {if (Boolean.valueOf(jimdoUndefined)) {javascriptInjector.injectScript(screen, "my_script.js", null);}}@Override public String javascriptInterfaceMethodName() {// This corresponds to LegacyCallbackInterfaceHelper.jimdoDefined()return "jimdoDefined";}});
首先,程序注入一个javascript方法去检查有脚本定义的一个变量是否被定义,然后根据返回的结果来决定是否进行javascript注入。
翻译:@flyouting 
时间:2014/03/20 
源地址