我们在native与网页相结合开发的过程中,难免会遇到关于WebView一些共通的问题。就我目前开发过程中遇到的问题以及最后得到的优化方案都将在这里列举出来。有些是老生常谈,有些则是个人摸索得出解决方法。下面就是整理得到的些干货。
故在WebView初始化时设置如下代码:
public void int () { if(Build.VERSION.SDK_INT >= 19) { webView.getSettings().setLoadsImagesAutomatically(true); } else { webView.getSettings().setLoadsImagesAutomatically(false); } }同时在WebView的WebViewClient实例中的onPageFinished()方法添加如下代码:
@Override public void onPageFinished(WebView view, String url) { if(!webView.getSettings().getLoadsImagesAutomatically()) { webView.getSettings().setLoadsImagesAutomatically(true); } }从上面的代码,可以看出我们对系统API在19以上的版本作了兼容。因为4.4以上系统在onPageFinished时再恢复图片加载时,如果存在多张图片引用的是相同的src时,会只有一个image标签得到加载,因而对于这样的系统我们就先直接加载。
@Override public void onReceivedError (WebView view, int errorCode, String description, String failingUrl) { super.onReceivedError(view, errorCode, description, failingUrl); loadDataWithBaseURL(null, "", "text/html", "utf-8", null); mErrorFrame.setVisibility(View.VISIBLE); }从上面可以看出,我们先使用loadDataWithBaseURL清除掉默认错误页内容,再让我们自定义的View得到显示(mErrorFrame为蒙在WebView之上的一个LinearLayout布局,默认为View.GONE)。
public boolean existVerticalScrollbar () { return computeVerticalScrollRange() > computeVerticalScrollExtent(); }computeVerticalScrollRange得到的是可滑动的最大高度,computeVerticalScrollExtent得到的是滚动把手自身的高,当不存在滚动条时,两者的值是相等的。当有滚动条时前者一定是大于后者的。
@Override protected void onScrollChanged(int newX, int newY, int oldX, int oldY) { super.onScrollChanged(newX, newY, oldX, oldY); if (newY != oldY) { float contentHeight = getContentHeight() * getScale(); // 当前内容高度下从未触发过, 浏览器存在滚动条且滑动到将抵底部位置 if (mCurrContentHeight != contentHeight && newY > 0 && contentHeight <= newY + getHeight() + mThreshold) { // TODO Something... mCurrContentHeight = contentHeight; } } }上面mCurrContentHeight用于记录上次触发时的网页高度,用来防止在网页总高度未发生变化而目标区域发生连续滚动时会多次触发TODO,mThreshold是一个阈值,当页面底部距离滚动条底部的高度差<=这个值时会触发TODO。
private void loadWithAccessLocal(final String htmlUrl) { new Thread(new Runnable() { public void run() { try { final String htmlStr = NetService.fetchHtml(htmlUrl); if (htmlStr != null) { TaskExecutor.runTaskOnUiThread(new Runnable() { @Override public void run() { loadDataWithBaseURL(htmlUrl, htmlStr, "text/html", "UTF-8", ""); } }); return; } } catch (Exception e) { Log.e("Exception:" + e.getMessage()); } TaskExecutor.runTaskOnUiThread(new Runnable() { @Override public void run() { onPageLoadedError(-1, "fetch html failed"); } }); } }).start(); }上面有几点需要注意:
20955-20968/xx.xxx.xxx E/webcoreglue﹕ Should not happen: no rect-based-test nodes found解决这个问题的办法是继承WebView类,在子类覆盖onTouchEvent方法,填入如下代码:
@Override public boolean onTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onScrollChanged(getScrollX(), getScrollY(), getScrollX(), getScrollY()); } return super.onTouchEvent(ev); }该方法的最先提出在 WebView in ViewPager not receive user inputs。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null); }
@Override public boolean onTouchEvent(MotionEvent ev) { boolean ret = super.onTouchEvent(ev); if (mPreventParentTouch) { switch (ev.getAction()) { case MotionEvent.ACTION_MOVE: requestDisallowInterceptTouchEvent(true); ret = true; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: requestDisallowInterceptTouchEvent(false); mPreventParentTouch = false; break; } } return ret; } public void preventParentTouchEvent () { mPreventParentTouch = true; }代码控制的关键在于mPreventParentTouch这个变量,mPreventParentTouch默认为false,当用户touchdown页面元素时通知该WebView将mPreventParentTouch设置为true。示意代码如下:
<script type="text/javascript"> document.getElementById("targetEle").addEventListener("touchstart", function(ev) { HostApp.preventParentTouchEvent(); // 通知WebView阻止祖先对其Touch事件的拦截 } ); document.getElementById("targetEle").addEventListener("touchmove", function(ev) { // todo something on this page } ); </script>关于web页面如何通知WebView(即调用Java方法)请参看 Android WebView开发问题及优化汇总第8条。
刚提到了上面是一种简单的做法,并不能很好的解决手指滑动过快带来的误操作问题,即当用户快速地滑动时,还是有一定机率会出现ViewPager拦截TouchMove事件而发生了Tab切换而非页面元素做出了响应。要完美解决此问题,就要用到稍微复杂一点的方法(仅是整体消息传递流程复杂一点)。
首先假设在ViewPager之上还有一个父元素叫做ParentViewOnViewPager,当我们接收到页面preventParentTouchEvent通知时就先于ViewPager而进行拦截。如下:
ParentViewOnViewPager.java public class ParentViewOnViewPager extends FrameLayout { private MineWebView mDispatchWebView; public void preventParentTouchEvent (WebView view) { mDispatchWebView = (MineWebView)view; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_MOVE && mDispatchWebView != null) { mDispatchWebView.ignoreTouchCancel(true); return true; } return false; } @Override public boolean onTouchEvent(MotionEvent ev) { if (mDispatchWebView != null){ switch (ev.getAction()) { case MotionEvent.ACTION_MOVE: mDispatchWebView.onTouchEvent(ev); break; default: mDispatchWebView.ignoreTouchCancel(false); mDispatchWebView.onTouchEvent(ev); mDispatchWebView = null; break; } return true; } return super.onTouchEvent(ev); } }即当ParentViewOnViewPager接收到通知时,发起TouchEvent拦截,将拦截到的Touch事件转嫁到装载页面的mDispatchWebView进行事件派发。这样就直接跳过了ViewPager这一层。这里需要注意的是 当ParentViewOnViewPager发起拦截时,WebView会接收到一个TouchCancel事件,WebView应该忽略这个事件,以避免页面接收到这个事件而打断整个处理流程。如下代码所示:
MineWebView.java public class MineWebView extends WebView { boolean mIgnoreTouchCancel; public void ignoreTouchCancel (boolean val) { mIgnoreTouchCancel = val; } @Override public boolean onTouchEvent(MotionEvent ev) { return ev.getAction() == MotionEvent.ACTION_CANCEL && mIgnoreTouchCancel || super.onTouchEvent(ev); } }另外针对这种解决方案,页面端的JS脚本不用做任何变动。