首页 > 代码库 > 从ViewPager嵌套RecyclerView再嵌套RecyclerView看安卓事件分发机制

从ViewPager嵌套RecyclerView再嵌套RecyclerView看安卓事件分发机制

这两天伟大的PM下了一个需求,在一个竖滑列表里实现一个横向滑动的列表,没错,又是这种常见但是又经常被具有着强烈责任心和职业操守程序员所嗤之以鼻的效果,废话不多说,先上图:
技术分享
实现的方式很多,因为项目中已经ViewPager+RV实现基本框架,所以现我也选择再添加一个RV实现相应的效果。

不过在写代码之前,先预估一下这个效果所有的坑。
VP是横向滑动的,RV是竖向滑动的,那么现在再添加一个横向滑动的RV,肯定会有滑动冲突,主要表现在

VP和横向滑动RV 的冲突,因为两者都是横向滑动的,肯定有冲突,无法判断哪个去滑动
竖向RV和横向RV的冲突,如果之前VP和竖向RV的冲突已经解决,那么现在只能我自己解决了
当横向RV滑动到最后一个item时候,应当让VP滑动到第二页,不能卡死在那里。

以上就是依靠我拙劣的开发经验所预先预估出来的坑,毕竟,滑动冲突这个字眼对安卓开发人员来说简直太敏感了,只要滑动方向不一致,就会脑海里展现,肯定有冲突了!
但是,这里先预先说明一下,我们其实按照最基本的写完就ok了,根本不用处理冲突。
WHAT??!!,准本撸起管子大干一炮的时候,发现不用解决,虽然省心省力,但是心里不爽啊。为啥呢,是哪里把滑动冲突处理了,是VP还是RV呢?

一开始的时候我是趋向于RV的,因为从子View层面去处理滑动冲突更好处理一点。但是事件的分发机制是隧道传播,冒泡处理形式,也就是说,先把事件传给上层View,上层View如果不处理那么传给下层View,下层View处理,则消耗掉事件,不处理,则返回给上层处理,直至有人处理完。

既然我们并没有复写任何关于滑动事件的方法,我们想弄清楚原因,只能从源码里面找,在源码里打断点,在打断点之前,你必须对安卓的事件分发有一点点的理解,下面罗列一下事件分发常见的方法:

dispatchTouchEvent(),事件的分发,返回TRUE表示事件被消耗,不向下分发
onTouchEvent(),返回true表示事件被消耗,事实上这个的返回值决定着dispatchTouchEvent()的返回值,看源码就能知道
除了上述几个方法,ViewGroup还有一个onInterceptTouchEvent方法,表示是否拦截事件,返回true自然也是拦截了
ViewGroup默认是不拦截任何事件的,View默认是要处理所有事件的。

关于更详细的总结,可以看一下这篇博客 安卓事件分发机制

接下来开始打断点,打开VP的源码,发现他是继承自ViewGroup的。这就说明,他默认是不拦截任何事件的。
技术分享

看到这就应该敏锐的觉察到,如果VP没有做任何的滑动效果,那么他就跟一个LinearLayout一样。所有的事件都会默认下发到他的子view。但是他现在有滑动效果,那么他是怎么下发到RV的呢,是他没管让RV处理了,还是自己处理好,不让RV接受事件呢?

打好断点:
技术分享

首先卖个关子,这个断点打的是有问题的。
同样的方式,打开RV源码,发现他也是继承ViewGroup的,但是我在找他的OnInterceptTouchEvent()方法的时候并没有找到他复写该方法,看来他默认也是不拦截事件传递的?

既然找不到OnInterceptTouchEvent()方法,那我们就找OntouchEvent方法,找到后,打断点如下:

技术分享
这个断点打的也有问题
顺便看了一下OnTouch事件的返回值,发现
技术分享
巧了,默认返回TRUE,看来默认情况下,RV是要处理所有事件的。

所有的断点打点完毕,开始调试,

技术分享

一步一步走,发现
技术分享
代码走进了这个分支里面,看见里面好多变量和一些方法我就仔细调试,看值,一行行的去理解,结果调试完了才发现,这!并!没!有!什!么!卵!用!
如果你因此而被繁杂的源代码绕进去,那你真的会被吓住,你要知道RV可是有10000多行的代码的,思考一下,为什么我说的断点有问题?就是因为这个。我们现在要研究的是横向滑动为什么没有出现滑动冲突,事实上我们只需要在ACTION.MOVE分支打断点就可以了,其他可以直接掠过。
*

这是我打断点遇到的第一个坑,毕竟以前没在源码里打过断点,一看源码这么复杂又看不懂只能乱打一通,慢慢看,事实上,这样只能让你望而却步。

*
进入到Action.Move分支,在分析这个分支代码之前,我们必须时刻谨记我们想要达到的目标以及我们所在的方法是干什么的,我们现在在IntercepetTouch方法里,这个方法是决定是否拦截事件的,返回TRUE表示拦截,返回false表示不拦截,所以,我们先不看这个分支的代码,毕竟50多行代码,绕进去你就没耐心了。
先去瞅一眼返回值,发现是个变量,mIsBeingDragged,默认是false,这和我们之前说的,ViewGroup默认不拦截所有事件不谋而合。
技术分享
既然返回值由mIsBeingDragged决定,那么我们先全局搜索一下该参数在哪里赋过值,crtl+f 全局搜索,发现除了在ActionDown事件里和ActionMove事件里,其他再无赋值的地方,所以,这就为我们排查提供了方便,ActionDown事件已经被我们排除,不需要去分析,只看ActionMove里面的

case MotionEvent.ACTION_MOVE:
                if (!mIsBeingDragged) {
                    final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                    if (pointerIndex == -1) {
                        // A child has consumed some touch events and put us into an inconsistent
                        // state.
                        needsInvalidate = resetTouch();
                        break;
                    }
                    final float x = ev.getX(pointerIndex);
                    final float xDiff = Math.abs(x - mLastMotionX);
                    final float y = ev.getY(pointerIndex);
                    final float yDiff = Math.abs(y - mLastMotionY);
                    if (DEBUG) {
                        Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
                    }
                    if (xDiff > mTouchSlop && xDiff > yDiff) {
                        if (DEBUG) Log.v(TAG, "Starting drag!");
                        mIsBeingDragged = true;
                        requestParentDisallowInterceptTouchEvent(true);
                        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
                                mInitialMotionX - mTouchSlop;
                        mLastMotionY = y;
                        setScrollState(SCROLL_STATE_DRAGGING);
                        setScrollingCacheEnabled(true);

                        // Disallow Parent Intercept, just in case
                        ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                    }
                }
                // Not else! Note that mIsBeingDragged can be set above.
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                    final float x = ev.getX(activePointerIndex);
                    needsInvalidate |= performDrag(x);
                }
                break;

代码还是蛮多的,但是我们只需要分析给mIsBeingDragged赋值以及有返回值的地方就可以了,所以其实核心就这两段代码,

if (dx != 0 && !isGutterDrag(mLastMotionX, dx)
                        && canScroll(this, false, (int) dx, (int) x, (int) y)) {
                    // Nested view has scrollable area under this point. Let it be handled there.
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mIsUnableToDrag = true;
                    return false;
                }

if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                    if (DEBUG) Log.v(TAG, "Starting drag!");
                    mIsBeingDragged = true;
                    requestParentDisallowInterceptTouchEvent(true);
                    setScrollState(SCROLL_STATE_DRAGGING);
                    mLastMotionX = dx > 0
                            ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop;
                    mLastMotionY = y;
                    setScrollingCacheEnabled(true);
                }

结合我们现在出现的情况,ViewPager嵌套RV没有出现滑动冲突,肯定是返回了false,所以,第二段代码其实也可以废除掉,不分析的,但是我为了装逼还是要说一下,if条件就是在判断滑动的方向,x方向的滑动距离如果大于最小的滑动距离,并且x方向滑动距离的0.53若大于y方向距离,就判定为横向滑动,那么就拦截该事件,mIsBeingDragged置为True ,但是这还不够,该方法让然调用了 requestParentDisallowInterceptTouchEvent(true);方法,这个方法就是告诉父控件,我要开始装逼了,你不要管我,剩下的事情我来处理就好了,你甭管。我个人理解就是双保险吧,至于其他代码,不用分析了,因为和我们这次的分析无关。这段代码也就是拦截了横向滑动事件,毕竟VP就是干这个事情的。

所以接下来就很清楚了,问题肯定出现在第一段代码里,判断语句有问题 if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && canScroll(this, false, (int) dx, (int) x, (int) y))
dx!=0不用说了,我们去分析一下isGutterDrag(mLastMotionX, dx) 和canScroll(this, false, (int) dx, (int) x, (int) y)) ,这里就不做具体分析了,isGutterDrag(mLastMotionX, dx)是判断是否在两个页面之间的缝隙内移动的,所以肯定返回false,那么一定是这个canScroll(this, false, (int) dx, (int) x, (int) y)返回了true。进源码看一眼

 protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        if (v instanceof ViewGroup) {
            final ViewGroup group = (ViewGroup) v;
            final int scrollX = v.getScrollX();
            final int scrollY = v.getScrollY();
            final int count = group.getChildCount();
            // Count backwards - let topmost views consume scroll distance first.
            for (int i = count - 1; i >= 0; i--) {
                // TODO: Add versioned support here for transformed views.
                // This will not work for transformed views in Honeycomb+
                final View child = group.getChildAt(i);
                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()
                        && y + scrollY >= child.getTop() && y + scrollY < child.getBottom()
                        && canScroll(child, true, dx, x + scrollX - child.getLeft(),
                                y + scrollY - child.getTop())) {
                    return true;
                }
            }
        }

        return checkV && ViewCompat.canScrollHorizontally(v, -dx);
    }

看着挺吓人,乍一眼不知道是干嘛用的,还用到了递归。但是我们看见里面有getChildCount这个函数,说明肯定跟子view 是相关的,所以呢,这个肯定是来判断VP里的子view的,名字叫canScroll,大致就可以猜到,这个函数是用来判断子View是不是可以滚动的,看note也就能知道:

**
     * Tests scrollability within child views of v given a delta of dx.
     *
     * @param v View to test for horizontal scrollability
     * @param checkV Whether the view v passed should itself be checked for scrollability (true),
     *               or just its children (false).
     * @param dx Delta scrolled in pixels
     * @param x X coordinate of the active touch point
     * @param y Y coordinate of the active touch point
     * @return true if child views of v can be scrolled by delta of dx.
     */

return true if child views of v can be scrolled by delta of dx,说的很清楚了,如果子view可以滚动就会返回True,VP里的RV是个子view,并且可以横向滚动,自然就走进该分支,切IntercpetOntouch返回false,不拦截,交给子View处理,子View该咋处理就咋处理,不做分析了

到这里也就对为啥VP嵌套RV不会出现事件冲突有了一个大概的了解,总结一下就是

VP默认不会拦截事件
VP会拦截横向滑动事件,这是他的本能,但是这段代码之前,他又干了其他事情,就是判断他的子View是否能滚动,能滚动的话,是不会拦截Move事件的。
VP嵌套VP也不会出现滚动冲突,原因就是上面两条,不知道为啥网上会有VP套VP的滑动冲突,不理解,我自己写代码没发现。

至于RV的事件冲突,不做分析了,大致雷同,写博客好累,代码没有上传github,因为新来公司还没配置~有需要联系。。。。

大佬拍拍砖,写博客比写代码累多了,好多地方我自己都表述不清楚~~~> 还有RV嵌套RV的冲突怎么解决的,看完博客你们自己分析一下吧,权当锻炼。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    从ViewPager嵌套RecyclerView再嵌套RecyclerView看安卓事件分发机制