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

Posted 安卓小小鸟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从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的事件冲突,不做分析了,大致雷同。

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

课后话

一直想着写博客提高自己,但是慢慢的又忘了,写博客能把事情讲清楚是一件很难的事情,我自己写也权当是做笔记而已,因为现在压力太大了,看着比自己小两三岁的一个个技术那么牛逼,自己却这么菜,而且我年纪也大了,不努力 一把房子是肯定买不起了,所以今年的计划是学习学习再学习,一年当成两年用,让自己更有竞争力。
这篇博客读完应该对一些小菜鸟有所帮助吧,那些觉得读取源码很累的小菜鸟通过这篇文章应该要学会抽丝剥茧,就像这篇里的VP,我们并没有去一行行的分析代码,而是先找关键点,比如MOVE,比如返回值,这个是最关键的,找到返回值,再全局查找可能赋值的地方,一一排查,缩小范围,只分析需要的代码,而不是一个个分析,那样真的很累。

我自己能分析出来原因其实我还是很诧异的,如果你看源代码还比较有难度,可能需要增强一下基础知识吧,不要着急,慢慢来。倘若你不知道安卓事件分发,你也不可能分析的这么块,所以,不要觉得源码难(事实上真的很难~),先巩固自己,一切都会豁然开朗

以上是关于从ViewPager嵌套RecyclerView再嵌套RecyclerView看安卓事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章

Android 中ViewPager嵌套RecyclerView出现滑动冲突的解决方案

ViewPager2嵌套RecyclerView滑动冲突解决办法

嵌套的 RecyclerView 滚动无法向下滚动 ViewPager2 的 BottomSheetBehavior

ViewPager2 TabLayout Fragment RecyclerView滑动冲突

Android 嵌套滚动NestedScrollView+TabLayout+ViewPager+Fragment+RecyclerView 实现京东美团首页效果Tab页滚动到顶部时自动吸附

Android 嵌套滚动NestedScrollView+TabLayout+ViewPager+Fragment+RecyclerView 实现京东美团首页效果Tab页滚动到顶部时自动吸附