RecyclerView嵌套WebView的两种解决方案
前言
众所周知,RecyclerView是可以上下滑动的(当然根据对LayoutManager设置的不同也可以左右滑动),而WebView也是可以上下左右滑动的。如果RecyclerView和WebView相互嵌套(即RecyclerView的一个条目为WebView),就会产生滑动冲突。具体表现就是只有RecyclerView能滑动,WebView的滑动事件被拦截了。原因也很好理解,如果你是RecyclerView的设计者,你也不会默认把滑动事件交给itemView去处理,因为这样很容易乱套,会出现很多奇奇怪怪的bug。RecyclerView对事件具体的处理策略,可以自行查看其源码。下面直接开门见山的说解决方案。
解决方案一
方案一其实很简单,在布局里面设置WebView的高度为“wrap_content”。至于为什么这样就可以,我们先来复习一下wrap_content和match_parent
-
wrap_content
wrap content翻译成汉语就是“包裹内容”,WebView的内容就是网页的内容,如果WebView的高度设置为“wrap_content”,那WebView的高度就是网页内容的高度。 -
match_parent
match parent即匹配父窗体,父控件有多高,高度设置成match_parent的View就有多高。
我们假设RecyclerView有两个条目(其中一个是WebView)。此时对WebView的高度设成wrap_content和match_parent时,对比如下:
match_parent和wrap_content.png当WebView高度设置成wrap_content时,WebView加载的网页的内容在WebView里全部展现了,只需要滑动RecyclerView,就可以查看到未显示的内容了。
如果设置成match_parent,网页的内容并没有全部展示在WebView当中,需要滑动WebView来展示没有展现的剩下的内容;而此时WebView并不会获得滑动事件,所以剩下的内容永远也没有展现的机会了。
既然wrap_content能完美解决,又如此简单,就用这种方案好了,为什么还会有方案二呢?wrap_content会有些问题,就我发现的:
1,如果网页会有弹窗,弹窗会显示在网页的正中间,也就是WebView的正中间,对照上图(1),正常情况下不会显示在屏幕范围内,需要向上滑动一段才能看见弹窗,这样对用户是不友好的;
2,会造成部分JS代码执行错误。
如果设置成match_parent就没有这些问题;下面剩下的问题就是解决滑动冲突,在合适的时候将RecyclerView的事件传递给WebView。即下面的解决方案二。
解决方案二
其实思路很简单,重写RecyclerView的onTouchEvent方法,在合适的时候将事件传递给WebView。但是这样做需要写个自定义的RecyclerView然后覆盖onTouchEvent方法,比较麻烦。
View对外提供有setOnTouchListener的接口,只需要传一个OnTouchListener的对象,实现onTouch方法,对事件进行处理即可。
OnTouchListener的优先级比onTouchEvent的优先级要高,可以参见View的源码的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
//代码省略
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//优先调用 li.mOnTouchListener.onTouch(this, event),如果返回true,就不会调用onTouchEvent了
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//
if (!result && onTouchEvent(event)) {
result = true;
}
//省略代码
return result;
}
接下来的难点就是在合适的时候将事件传递给WebView了。直接看代码注释吧:
private class RecyclerViewOnTouchListener implements View.OnTouchListener {
private int mLastY;
private int mCurrentY;
@Override
public boolean onTouch(View v, MotionEvent event) {
WebViewHolder webViewHolder = mAdapter.getWebViewHolder();
if(webViewHolder == null) {
return false;
}
//获取WebView对象,以便将事件传递给他
WebView webView = (WebView) webViewHolder.itemView.findViewById(R.id.web_view);
//获取WebView所在item的顶部相对于其父控件(即RecyclerView的父控件)的距离
int itemViewTop = webViewHolder.itemView.getTop();
if(itemViewTop > 0) {
return false;
}
if(itemViewTop < 0) {
webViewHolder.itemView.scrollTo(0, 0);
return false;
}
//计算dy,用来判断滑动方向。dy<0-->向上滑动;dy>0-->向下滑动。
int dy = 0;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
mCurrentY = (int) event.getY();
dy = mCurrentY - mLastY;
mLastY = mCurrentY;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
dy = (int) (event.getY() - mLastY);
mLastY = 0;
mCurrentY = 0;
break;
}
Log.d(TAG, "dy = " + dy);
Log.d(TAG, "itemViewTop = " + itemViewTop);
//如果WebView顶部距离其父控件距离未0,即WebView顶部滑动到RecyclerView父控件顶部重合时,
// 此时需要拦截滑动事件交给WebView处理。
if(itemViewTop == 0) {
if(shouldIntercept(webView, dy)) {
webView.onTouchEvent(event);
return true;
}
}
return false;
}
/**
* 是否拦截滑动事件,判断的逻辑是:<br/>
* 1,如果是向上滑动,并且webview能够向上滑动,则拦截事件;<br/>
* 2,如果是向下滑动,并且webview能够向下滑动,则拦截事件。
* @param view 判断能够滑动的view
* @param dy 滑动间距
* @return true拦截,false不拦截。
*/
private boolean shouldIntercept(View view, int dy) {
//canScrollVertically方法的第二个参数direction,传1时返回是否能够向上滑动,传-1时返回能否向下滑动。
//dy<0-->向上滑动;dy>0-->向下滑动。
boolean scrollUp = dy < 0 && ViewCompat.canScrollVertically(view, 1);
boolean scrollDown = dy > 0 && ViewCompat.canScrollVertically(view, -1);
return scrollUp || scrollDown || dy == 0;
}
}
接下来把该OnTouchListener设置给RecyclerView就可以了。
recyclerView.setOnTouchListener(new RecyclerViewOnTouchListener());
具体逻辑代码注释已经写的很清楚了,这里就不再啰嗦了。
结语
方案二逻辑比较复杂,没有完整测试,不知道有没有bug。方案一比较简单直接,如果你要加载的网页环境比较简单,没有弹窗,就直接用方案一吧,开发工作量也要小很多。
另外本人才疏学浅,可能有表述不当甚至理解错误的地方,欢迎指正,共同进步。
江湖规矩,源码见 github