RecyclerView嵌套WebView的两种解决方案

作者: 胡几手 | 来源:发表于2017-07-25 22:36 被阅读3109次

    前言

    众所周知,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

    相关文章

      网友评论

      • 易公子清秋:这样webview显示不全
      • ea613d9350b0:使用wrap_content根本就不显示网页好么!!!!!!!!!!!!!
        胡几手: @碧云天丶 你贴的博客的实现方式也是把WebView的高度设置成wrapcontent,只不过是用的代码的方式。
        碧云天丶:http://blog.csdn.net/u013200864/article/details/51766931
        多找找博客,小伙纸

      本文标题:RecyclerView嵌套WebView的两种解决方案

      本文链接:https://www.haomeiwen.com/subject/lopmkxtx.html