@flyouting
2014-03-20T16:01:47.000000Z
字数 3700
阅读 7463
最近一段时间都在捣腾 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 API
public static interface ValueCallbackAdapter {
void evaluateResult(String value);
String javascriptInterfaceMethodName();
}
}
如果应用使用了什么注入框架,比如 Dagger,在注入之前,在适配器初始化时,我们需要添加一个if语句。
@Module(injects = WebViewFragment.class)
public class WebViewModule {
...
@Provides @Singleton
public 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);
}
@Override
public 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 js
public 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
源地址