协调布局-嵌套滑动源码解读

一 协调布局示例

从最简单的协调布局嵌套滑动开始,首先看最简单的协调布局。
最外层一个CoordinatorLayout布局,它的子View只有AppBarLayoutRecyclerView,这就实现了最简单的协调布局。具体布局XML布局如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.appcompat.widget.AppCompatImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:scaleType="centerCrop"
            android:src="@drawable/mm1"
            app:layout_scrollFlags="scroll" />
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/test_appbar_no_child_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

效果图如下:
在这里插入图片描述

从布局上看,手指既可以通过滑动AppBarLayout组件,又可以通过滑动RecyclerView组件来达到上下两个组件嵌套滑动的效果。本篇所有的分析都是基础这个简单的协调布局来理解。

二 RecyclerView的Behavior设置

从布局文件中,RecyclerView设置了一个Behavior值,appbar_scrolling_view_behavior这个值对应的值是:

<string name="appbar_scrolling_view_behavior" translatable="false">com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior</string>

可以看出对应的值是一个类,对应的AppBarLayout类中的静态内部类ScrollingViewBehavior,通过在布局中给RecyclerView设置上面的属性,就给RecyclerView设置了自己的Behavior,这个Behavior就是ScrollingViewBehavior

问题来了,ScrollingViewBehavior是如何设置给RecyclerView的呢?
这就需要看CoordinatorLayout里面的静态内部类LayoutParams。在CoordinatorLayoutLayoutParams的构造方法中,源码如下:

LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    final TypedArray a = context.obtainStyledAttributes(attrs,
            R.styleable.CoordinatorLayout_Layout);
	......
    mBehaviorResolved = a.hasValue(
            R.styleable.CoordinatorLayout_Layout_layout_behavior);
    if (mBehaviorResolved) {
        mBehavior = parseBehavior(context, attrs, a.getString(
                R.styleable.CoordinatorLayout_Layout_layout_behavior));
    }
    a.recycle();

    if (mBehavior != null) {
        mBehavior.onAttachedToLayoutParams(this);
    }
}

从上面的代码可以看出,在创建RecyclerViewLayoutParams对象时,会解析布局文件中设置的layout_behavior属性,然后通过parseBehavior方法进行解析。再看parseBehavior方法的源码:

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    if (TextUtils.isEmpty(name)) {
        return null;
    }

    final String fullName;
    if (name.startsWith(".")) {
        // Relative to the app package. Prepend the app package name.
        fullName = context.getPackageName() + name;
    } else if (name.indexOf('.') >= 0) {
        // Fully qualified package name.
        fullName = name;
    } else {
        // Assume stock behavior in this package (if we have one)
        fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                ? (WIDGET_PACKAGE_NAME + '.' + name)
                : name;
    }

    try {
        Map<String, Constructor<Behavior>> constructors = sConstructors.get();
        if (constructors == null) {
            constructors = new HashMap<>();
            sConstructors.set(constructors);
        }
        Constructor<Behavior> c = constructors.get(fullName);
        if (c == null) {
            final Class<Behavior> clazz =
                    (Class<Behavior>) Class.forName(fullName, false, context.getClassLoader());
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}

这个方法的源码也很简单,就是根据设置的layout_behavior值解析出相对应的类,然后通过反射创建该类的对象实例。

以上就是RecyclerView如何设置Behavior的源码解析。

三 AppBarLayout的Behavior设置

其实上面的布局中AppBarLayout也有Behavior的,只不过是源码中默认设置的,AppBarLayout源码如下:

public CoordinatorLayout.Behavior<AppBarLayout> getBehavior() {
	return new AppBarLayout.Behavior();
}

上面这个方法是AppBarLayout源码中公开方法,但是设置Behavior不是在AppBarLayout中,而是在CoorinatorLayout中完成,CoorinatorLayout源码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    prepareChildren();
    ......
}

private void prepareChildren() {
    ......
    for (int i = 0, count = getChildCount(); i < count; i++) {
        final View view = getChildAt(i);

        final LayoutParams lp = getResolvedLayoutParams(view);
        lp.findAnchorView(this, view);

        mChildDag.addNode(view);
		......
    }
	.......
}

LayoutParams getResolvedLayoutParams(View child) {
    final LayoutParams result = (LayoutParams) child.getLayoutParams();
    if (!result.mBehaviorResolved) {
        if (child instanceof AttachedBehavior) {
            Behavior attachedBehavior = ((AttachedBehavior) child).getBehavior();
            if (attachedBehavior == null) {
                Log.e(TAG, "Attached behavior class is null");
            }
            result.setBehavior(attachedBehavior);
            result.mBehaviorResolved = true;
        } else {
            // The deprecated path that looks up the attached behavior based on annotation
            Class<?> childClass = child.getClass();
            DefaultBehavior defaultBehavior = null;
            while (childClass != null
                    && (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class))
                    == null) {
                childClass = childClass.getSuperclass();
            }
            if (defaultBehavior != null) {
                try {
                    result.setBehavior(
                            defaultBehavior.value().getDeclaredConstructor().newInstance());
                } catch (Exception e) {
                    Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName()
                            + " could not be instantiated. Did you forget"
                            + " a default constructor?", e);
                }
            }
            result.mBehaviorResolved = true;
        }
    }
    return result;
}

从上面的代码中可以看出,在CoordinatorLayout中的onMeasure方法中,调用了prepareChildren方法,这个方法中循环遍历子View,并对每个子View调用getResolvedLayoutParams方法,在getResolvedLayoutParams方法中解析各个子View的Behavior。这一行代码Behavior attachedBehavior = ((AttachedBehavior) child).getBehavior();就是设置AppBarLayoutBehavior的,AppBarLayout是实现了AttachedBehavior接口的,这个接口也只有一个方法getBehavior()

上面的代码中还有一种设置Behavior的方法,就是通过DefaultBehavior注解。如果我们看协调布局CoordinatorLayout比较早一点的源码版本,就会发现AppBarLayout其实是通过DefaultBehavior注解设置Behavior的。目前文中使用的代码已经不是通过注解设置了,而是AppBarLayout实现AttachedBehavior接口的方式设置Behavior的。

通过上面的分析,我们就知道
AppBarLayout有自己的Behavior,就是AppBarLayout.Behavior对象。
RecyclerView有自己的Behavior,就是AppBarLayout.ScrollingViewBehavior对象。
这一点非常重要,务必记住。

四 示例分析

示例代码中嵌套滑动会有两种情况发生:
1 手指按下AppBarLayout组件上,上下滑动
2 手指按下RecyclerView组件上,上下滑动

还有一种特殊情况,手指按下RecyclerView上面,滑动到AppBarLayout滑出整个界面,然后RecyclerView自己滑动的情况。这种情况是上面的特殊情况。明白了上面两种情况,这种情况就不在话下了。

在说明上面两种情况之前,我们首先回忆一下Android事件分发机制,手指按下屏幕的时候,自然会触发一个ACTION_DOWN事件,抛开Activity层的处理逻辑不提,首先接收到ACTION_DOWN事件的肯定是CoordinatorLayout组件,对不对?

如果手指按下触发的ACTION_DOWN事件是在AppBarLayout组件的区域内部,那CoordinatorLayout组件应该把ACTION_DOWN事件分发给AppBarLayout组件。

如果手指按下触发的ACTION_DOWN事件是在RecyclerView组件的区域内部,那CoordinatorLayout组件应该把ACTION_DOWN事件分发给RecyclerView组件。

按道理上面的流程我们不看CoordinatorLayout组件的源码,仅仅按照Android事件分发机制的原理来理解,也应该如此对不对?

通过上面的理解,没有看CoordinatorLayout组件的源码的情况下,我们大致知道ACTION_DOWN事件的分发肯定是上面的情况。那么对于接下来的ACTION_MOVE事件呢?也应该如此,谁拦截了ACTION_DOWN事件,接下来谁就处理ACTION_MOVE事件。

这就带来了几个问题:

1 AppBarLayout组件处理ACTION_MOVE时,自己在上下滑动的时候,RecyclerView组件是如何嵌套滑动的?
2 RecyclerView组件处理ACTION_MOVE时,自己上下滑动的时候,AppBarlayout组件是如何处理嵌套滑动的?
3 当AppBarLayout组件从显示到完全滑出屏幕的时候,RecyclerView是如何处理滑动的?
4 Behavior在嵌套滑动中起着什么样的作用?CoordinatorLayout组件又有什么作用?
5 协调布局自身抖动的Bug是什么原因产生的?如何解决?

五 源码分析

由于协调布局复杂,我们需要找到一个分析源码的突破口。从上面的分析我们知道,手指按下触发ACTION_DOWN事件首先是由CoordinatorLayout组件接收并处理的。对于处理的具体的方法,在Android事件分发机制中主要是三个方法,dispatchTouchEvent方法、onInterceptTouchEvent方法和onTouchEvent方法。

CoordinatorLayout源码中没有找到dispatchTouchEvent方法,并且在ViewGroup的源码中可以看出,只有很少的几个组件重写了dispatchTouchEvent方法,这也就是提醒我们,在自定义View的时候,尽量不要重写dispatchTouchEvent方法,除非你知道自己在做什么。

5.1 CoordinatorLayout组件的onInterceptTouchEvent方法分析

CoordinatorLayout组件的dispatchTouchEvent方法走的是ViewGroup源码的dispatchTouchEvent方法的逻辑。

Android事件分发机制中的分析,dispatchTouchEvent方法会调用自己的onInterceptTouchEvent方法和onTouchEvent方法,CoordinatorLayout组件虽然没有重写dispatchTouchEvent方法,但是重写了onInterceptTouchEvent方法和onTouchEvent方法。

首先查看CoordinatorLayout组件的onInterceptTouchEvent方法。因为这个方法会影响点击事件的逻辑。其源码如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    final int action = ev.getActionMasked();

    // Make sure we reset in case we had missed a previous important event.
    if (action == MotionEvent.ACTION_DOWN) {
        resetTouchBehaviors(true);
    }

    final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors(true);
    }

    return intercepted;
}

这里的源码非常好理解,当接收到一个ACTION_DOWN事件的时候,通过调用resetTouchBehaviors(true);来重制滑动的一些值,为接下来的嵌套滑动的事件做准备。然后调用performIntercept(ev, TYPE_ON_INTERCEPT)方法,该方法的返回值就是onInterceptTouchEvent方法的返回值。最后对于接收到的ACTION_UP或者ACTION_CANCEL事件再一次重置嵌套滑动的一些值。

先把performIntercept(ev, TYPE_ON_INTERCEPT)方法按下不说,先看看resetTouchBehaviors(true)方法的逻辑:

private void resetTouchBehaviors(boolean notifyOnInterceptTouchEvent) {
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if (b != null) {
            final long now = SystemClock.uptimeMillis();
            final MotionEvent cancelEvent = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            if (notifyOnInterceptTouchEvent) {
                b.onInterceptTouchEvent(this, child, cancelEvent);
            } else {
                b.onTouchEvent(this, child, cancelEvent);
            }
            cancelEvent.recycle();
        }
    }

    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        lp.resetTouchBehaviorTracking();
    }
    mBehaviorTouchView = null;
    mDisallowInterceptReset = false;
}

这个方法的名字就看出这个方法就是重置Behavior的触摸逻辑。代码可以看出,CoordinatorLayout循环遍历各个子View,并调用各个子view的Behavior的onInterceptTouchEvent方法或者b.onTouchEvent方法,代码逻辑上发出一个ACTION_CANCEL事件,并重置mBehaviorTouchView的值为null。mBehaviorTouchView这个变量保存的就是找到拦截事件的View。对应文中的示例就是AppBarLayout或者RecyclerView

这里这个方法是在onInterceptTouchEvent方法中调用的,所以其参数值notifyOnInterceptTouchEvent=true。如果这个方法在onTouchEvent方法中调用的,其参数值notifyOnInterceptTouchEvent=false

现在我们再回到onInterceptTouchEvent方法,该方法的返回值是以performIntercept方法的返回值作为结果返回的。performIntercept方法才是onInterceptTouchEvent方法的处理逻辑。其源码如下:

private boolean performIntercept(MotionEvent ev, final int type) {
    boolean intercepted = false;
    boolean newBlock = false;
    MotionEvent cancelEvent = null;
    final int action = ev.getActionMasked();
    final List<View> topmostChildList = mTempList1;
    getTopSortedChildren(topmostChildList);

    final int childCount = topmostChildList.size();
    for (int i = 0; i < childCount; i++) {
        final View child = topmostChildList.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
            if (b != null) {
                if (cancelEvent == null) {
                    final long now = SystemClock.uptimeMillis();
                    cancelEvent = MotionEvent.obtain(now, now,
                            MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                }
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        b.onInterceptTouchEvent(this, child, cancelEvent);
                        break;
                    case TYPE_ON_TOUCH:
                        b.onTouchEvent(this, child, cancelEvent);
                        break;
                }
            }
            continue;
        }

        if (!intercepted && b != null) {
            switch (type) {
                case TYPE_ON_INTERCEPT:
                    intercepted = b.onInterceptTouchEvent(this, child, ev);
                    break;
                case TYPE_ON_TOUCH:
                    intercepted = b.onTouchEvent(this, child, ev);
                    break;
            }
            if (intercepted) {
                mBehaviorTouchView = child;
            }
        }
		......
    }

    topmostChildList.clear();

    return intercepted;
}

performIntercept方法现在是在onInterceptTouchEvent方法中调用的,其第二个参数固定值为TYPE_ON_INTERCEPT。对于第一个参数来讲,它的值可能是ACTION_DOWN、ACTION_MOVE、ACTION_UP。

方法内部for循环遍历子View,调用每个子View的Behavior对应的onInterceptTouchEvent方法或者onTouchEvent方法。只要有一个子View对应的Behavior对应的方法返回true,要拦截事件,那么CoordinatorLayoutonInterceptTouchEvent方法就返回true。而对于上文中示例代码来说,CoordinatorLayout组件内部只有两个子View,所以performIntercept方法中遍历子View,调用对应子View的Behavior就是调用AppBarLayoutAppBarLayout.BehaviorRecyclerViewAppBarLayout.ScrollingViewBehavior。这一点需要明确,也非常关键。如果不理解的话,文章开头的部分已经说明。

有了以上的知识储备,现在回头总结到目前分析的源码,ACTION_DOWN事件的处理逻辑:
CoordinatorLayout组件首先接收到ACTION_DOWN事件,走它的dispatchTouchEvent方法,该方法CoordinatorLayout组件没有重写,走的是ViewGroupdispatchTouchEvent方法,该方法首先会调用onInterceptTouchEvent方法,这个方法CoordinatorLayout组件进行了重写,在onInterceptTouchEvent方法内部调用了performIntercept方法,循环遍历CoordinatorLayout组件的各个子View的Behavior。这样ACTION_DOWN事件就由CoordinatorLayout组件传递给了子View。子View对应的BehavioronInterceptTouchEvent方法判断是否需要拦截。

这里还有一点需要说明,CoordinatorLayout组件重写了onInterceptTouchEvent方法。但是该方法并不是在同一个事件系列里面每次都调用。onInterceptTouchEvent方法的调用是有几个条件的。在FLAG_DISALLOW_INTERCEPT标记位没有设置的情况下,第一个是ACTION_DOWN事件的时候,该方法会调用。第二个是mFirstTouchTarget对象不为空的时候。

FLAG_DISALLOW_INTERCEPT标记位一般也不会设置的,先忽略这个标记位。ACTION_DOWN事件上面的CoordinatorLayout组件的onInterceptTouchEvent方法会被调用,这个没有疑问。但是对于ACTION_MOVE和ACTION_UP事件,CoordinatorLayout组件的onInterceptTouchEvent方法不会被调用。
在这里插入图片描述
这张图中所示,就是到目前为止源码对于ACTION_DOWN事件的处理逻辑。

5.2 AppBarLayout.Behavior的onInterceptTouchEvent方法分析

AppBarLayout.Behavior类的源码如下:

public static class Behavior extends BaseBehavior<AppBarLayout> {
  public abstract static class DragCallback extends BaseBehavior.BaseDragCallback<AppBarLayout> {}

  public Behavior() {
    super();
  }

  public Behavior(Context context, AttributeSet attrs) {
    super(context, attrs);
  }
}

AppBarLayout.Behavior类继承自BaseBehavior类,而BaseBehavior类源码如下:

protected static class BaseBehavior<T extends AppBarLayout> extends HeaderBehavior<T> {
	......
}


abstract class HeaderBehavior<V extends View> extends ViewOffsetBehavior<V> {
	......
	public boolean onInterceptTouchEvent(
      @NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
    .......
    // Shortcut since we're being dragged
    if (ev.getActionMasked() == MotionEvent.ACTION_MOVE && isBeingDragged) {
      if (activePointerId == INVALID_POINTER) {
        // If we don't have a valid id, the touch down wasn't on content.
        return false;
      }
      int pointerIndex = ev.findPointerIndex(activePointerId);
      if (pointerIndex == -1) {
        return false;
      }

      int y = (int) ev.getY(pointerIndex);
      int yDiff = Math.abs(y - lastMotionY);
      if (yDiff > touchSlop) {
        lastMotionY = y;
        return true;
      }
    }

    if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
      activePointerId = INVALID_POINTER;

      int x = (int) ev.getX();
      int y = (int) ev.getY();
      isBeingDragged = canDragView(child) && parent.isPointInChildBounds(child, x, y);
      if (isBeingDragged) {
        lastMotionY = y;
        activePointerId = ev.getPointerId(0);
        ensureVelocityTracker();

        // There is an animation in progress. Stop it and catch the view.
        if (scroller != null && !scroller.isFinished()) {
          scroller.abortAnimation();
          return true;
        }
      }
    }

    if (velocityTracker != null) {
      velocityTracker.addMovement(ev);
    }

    return false;
  }
	......
}

通过上面的代码可以看出,AppBarLayout.Behavior类的onInterceptTouchEvent方法,实际调用的是HeaderBehavior类中的onInterceptTouchEvent方法。
主要看的是(ev.getActionMasked() == MotionEvent.ACTION_DOWN)这一行代码。对于ACTION_DOWN事件,当触摸区域也就是点击在AppBarLayout的区域内部,parent.isPointInChildBounds(child, x, y)会为true,这里是判断触摸点是否在AppBarLayout的区域内部。而对于canDragView(child)默认返回true,所以isBeingDragged会被赋值true。接下来isBeingDragged=true的情况下,判断AppBarLayout的滑动是否结束,如果没有结束,停止AppBarLayout滑动,直接AppBarLayout拦截事件返回true,否则就返回false。

5.3 AppBarLayout.ScrollingViewBehavior的onInterceptTouchEvent方法分析

对于RecyerViewAppBarLayout.ScrollingViewBehavior来说,没有重写父类的onInterceptTouchEvent方法,直接使用的就是CoordinatorLayout中的静态抽象类BehavioronInterceptTouchEvent方法。

public static abstract class Behavior<V extends View> {
	public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
      @NonNull MotionEvent ev) {
        return false;
    }
}

至此,分析的逻辑用下图表示:
在这里插入图片描述

到目前为止,ACTION_DOWN事件回到了CoordinatorLayoutdispatchTouchEvent方法。也就是ViewGroupdispatchTouchEvent方法。

CoordinatorLayoutonInterceptTouchEvent方法返回false
说明此时没有要拦截的View,此时根据ViewGroupdispatchTouchEvent方法的逻辑,就是遍历子View。如果点击区域在AppBarLayout的区域,就会调用AppBarLayoutdispatchTouchEvent方法。如果点击区域在RecyclerView的区域,就会调用RecyclerViewdispatchTouchEvent方法。

此时出现了两种。这两种情况分开分析。

5.4 AppBarLayout事件拦截逻辑

首先遍历AppBarLayout
AppBarLayout的源码中并没有重写dispatchTouchEvent方法和onInterceptTouchEvent方法,也就意味着即使点击区域在AppBarLayout区域内部,此时它的onInterceptTouchEvent方法依然返回false。

AppBarLayout返回false的情况下,再遍历RecyclerView。如果点击区域不在RecyclerView内部,直接返回false。如果点击区域在RecyclerView内部,接下来的第二种情况的分析才会有意义。

第二种情况先按下不说,继续来将点击区域在AppBarLayout的区域内部,此时他的onInterceptTouchEvent方法返回false,但是从表现上看,手指触摸在AppBarLayout区域内确实能够滑动AppBarLayout区域,这是怎么回事儿呢?既然AppBarLayoutonInterceptTouchEvent方法不拦截,那什么地方触发了事件拦截呢??

Android事件分发机制中的分析,我们知道,在触摸区域在AppBarLayout区域内的时候,RecyclerView不会拦截事件,因为触摸区域不再RecyclerView内部,又由于AppBarLayoutonInterceptTouchEvent方法返回false,并且AppBarLayout自身没有重写onTouchEvent方法,此时就跳出了循环遍历CoordinatorLayout中的dispatchTouchEvent方法遍历子View是否拦截的逻辑,并且没有找到拦截ACTION__DOWN事件的子View,根据Android事件分发机制中的分析,就会走到CoordinatorLayoutonTouchEvent方法的逻辑。

下面继续看CoordinatorLayoutonTouchEvent方法的源码

public boolean onTouchEvent(MotionEvent ev) {
    boolean handled = false;
    boolean cancelSuper = false;
    MotionEvent cancelEvent = null;

    final int action = ev.getActionMasked();

    if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
        final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if (b != null) {
            handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
        }
    }
    ......
    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors(false);
    }

    return handled;
}

删除了一些代码,CoordinatorLayoutonTouchEvent方法内部,首先调用performIntercept方法,第二个参数注意是TYPE_ON_TOUCH值,如果这个方法返回true,代表有子View拦截事件,此时会获取这个子View的Behavior,也就是mBehaviorTouchView的值,然后调用该BehavioronTouchEvent方法,该方法的返回值就是CoordinatorLayoutonTouchEvent方法的返回值。

对于performIntercept方法,我们之前分析过,再看它的源码如下:

private boolean performIntercept(MotionEvent ev, final int type) {
    boolean intercepted = false;
    boolean newBlock = false;
    MotionEvent cancelEvent = null;
    final int action = ev.getActionMasked();
    final List<View> topmostChildList = mTempList1;
    getTopSortedChildren(topmostChildList);

    final int childCount = topmostChildList.size();
    for (int i = 0; i < childCount; i++) {
        final View child = topmostChildList.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
            if (b != null) {
                if (cancelEvent == null) {
                    final long now = SystemClock.uptimeMillis();
                    cancelEvent = MotionEvent.obtain(now, now,
                            MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                }
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        b.onInterceptTouchEvent(this, child, cancelEvent);
                        break;
                    case TYPE_ON_TOUCH:
                        b.onTouchEvent(this, child, cancelEvent);
                        break;
                }
            }
            continue;
        }

        if (!intercepted && b != null) {
            switch (type) {
                case TYPE_ON_INTERCEPT:
                    intercepted = b.onInterceptTouchEvent(this, child, ev);
                    break;
                case TYPE_ON_TOUCH:
                    intercepted = b.onTouchEvent(this, child, ev);
                    break;
            }
            if (intercepted) {
                mBehaviorTouchView = child;
            }
        }
		......
    }

    topmostChildList.clear();

    return intercepted;
}

performIntercept方法内部,循环遍历各个子View的onTouchEvent方法,如果intercepted=true,说明有子View拦截了事件,mBehaviorTouchView = child,这个值就不会为空。

现在继续往下走,看AppBarLayout.BehavioronTouchEvent方法分析

5.5 AppBarLayout.Behavior的onTouchEvent方法分析

public boolean onTouchEvent(
    @NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
  boolean consumeUp = false;
  switch (ev.getActionMasked()) {
    case MotionEvent.ACTION_MOVE:
      final int activePointerIndex = ev.findPointerIndex(activePointerId);
      if (activePointerIndex == -1) {
        return false;
      }

      final int y = (int) ev.getY(activePointerIndex);
      int dy = lastMotionY - y;
      lastMotionY = y;
      // We're being dragged so scroll the ABL
      scroll(parent, child, dy, getMaxDragOffset(child), 0);
      break;
    case MotionEvent.ACTION_POINTER_UP:
      int newIndex = ev.getActionIndex() == 0 ? 1 : 0;
      activePointerId = ev.getPointerId(newIndex);
      lastMotionY = (int) (ev.getY(newIndex) + 0.5f);
      break;
    case MotionEvent.ACTION_UP:
      if (velocityTracker != null) {
        consumeUp = true;
        velocityTracker.addMovement(ev);
        velocityTracker.computeCurrentVelocity(1000);
        float yvel = velocityTracker.getYVelocity(activePointerId);
        fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
      }

      // $FALLTHROUGH
    case MotionEvent.ACTION_CANCEL:
      isBeingDragged = false;
      activePointerId = INVALID_POINTER;
      if (velocityTracker != null) {
        velocityTracker.recycle();
        velocityTracker = null;
      }
      break;
  }

  if (velocityTracker != null) {
    velocityTracker.addMovement(ev);
  }

  return isBeingDragged || consumeUp;
}

该方法先看最后的返回值,return isBeingDragged || consumeUp,我们上面分析了AppBarLayout.BehavioronInterceptTouchEvent方法,在onInterceptTouchEvent方法中,如果点击区域在AppBarLayout内部,会设置isBeingDragged=true。具体对应的代码如下:

isBeingDragged = canDragView(child) && parent.isPointInChildBounds(child, x, y);

这行代码就表明,如果点击区域在AppBarLayout内部,即使AppBarLayout.BehavioronInterceptTouchEvent方法返回了false,但是AppBarLayout.BehavioronTouchEvent方法返回true。这样后续的事件就会交给AppBarLayout.Behavior来处理。

具体说为什么后续事件交给AppBarLayout.Behavior来处理的原因是什么呢?
因为分析到目前为止,点击区域在AppBarLayout区域内部,此时由CoordinatorLayoutonTouchEvent方法内部,遍历子View,调用到了AppBarLayout.BehavioronTouchEvent方法,这方法对于ACTION_DOWN事件返回了true,那对于CoordinatorLayoutonTouchEvent方法也就返回了true,继续往上追上CoordinatorLayoutdispatchTouchEvent方法返回了true。那么后续的事件ACTION_MOVE和ACTION_UP事件,就会交给CoordinatorLayoutdispatchTouchEvent方法,然后调用CoordinatorLayout自己的onTouchEvent方法,继续往下追溯到AppBarLayout.BehavioronTouchEvent方法,而AppBarLayout.BehavioronTouchEvent方法,对于ACTION_MOVE事件,调用了scroll(parent, child, dy, getMaxDragOffset(child), 0);进行AppBarLayout滑动。这样一来整个事件就串联了起来,AppBarLayout就滑动了起来,直到整个事件序列结束。

AppBarLayout的具体滑动不展示分析,篇幅太长了。

以上AppBarLayout的事件处理逻辑,用下图来表示:
在这里插入图片描述

5.6 AppBarLayout的滑动抖动问题

但是对于AppBarLayout有一种特殊情况是,如果点击区域在AppBarLayout区域内,同时AppBarLayout的滑动事件没有结束,它的AppBarLayout.Behavior中的onInterceptTouchEvent方法返回true,表示它要拦截。
具体源码对应AppBarLayout.Behavior中的onInterceptTouchEvent方法,如下:

public boolean onInterceptTouchEvent(
      @NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
    ......
    if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
      activePointerId = INVALID_POINTER;

      int x = (int) ev.getX();
      int y = (int) ev.getY();
      isBeingDragged = canDragView(child) && parent.isPointInChildBounds(child, x, y);
      if (isBeingDragged) {
        lastMotionY = y;
        activePointerId = ev.getPointerId(0);
        ensureVelocityTracker();

        // There is an animation in progress. Stop it and catch the view.
        if (scroller != null && !scroller.isFinished()) {
          scroller.abortAnimation();
          return true;
        }
      }
    }

    if (velocityTracker != null) {
      velocityTracker.addMovement(ev);
    }

    return false;
  }

具体是这几行代码:

if (scroller != null && !scroller.isFinished()) {
  scroller.abortAnimation();
  return true;
}

这几行代码表示,如果AppBarLayout的滑动没有结束,就结束AppBarLayout的滑动,同时拦截事件,返回true。

但是这几行生效是有前提条件的,前提条件是isBeingDragged=true
而这个条件必须要求点击区域在AppBarLayout的内部,如果点击区域不再AppBarLayout的内部,即使AppBarLayout滑动没有结束,也不会通过代码让AppBarLayout滑动结束。

咋一看貌似没什么问题,但是仔细想想就有问题了。
如果AppBarLayout滑动没结束,此时点击在了RecyclerView上面会怎么样呢?如果紧接着RecyclerView进行了滑动,又会怎么样呢?

在这里插入图片描述
这个GIF图没有体现出来,很不明显的。具体操作是这样的,手指先向上Fling滑动,AppBarLayout还没有滑动结束的时候,立即点击RecyclerView向下Fling滑动,此时AppBarLayout向上Fling和RecyclerView向下Fling之间就冲突了,导致的现象是向上和向下的具体来回变化设置,导致布局上下抖动,影响用户体验。

具体解决方法,留在下文分析了RecyclerView的事件拦截逻辑再说。

5.7 RecyclerView的事件拦截逻辑

上文我们分析了CoordinatorLayoutonInterceptTouchEvent方法,分了两种情况。

CoordinatorLayoutonInterceptTouchEvent方法返回false
说明此时没有要拦截的View,此时根据ViewGroupdispatchTouchEvent方法的逻辑,就是遍历子View。如果点击区域在AppBarLayout的区域,就会调用AppBarLayoutdispatchTouchEvent方法。如果点击区域在RecyclerView的区域,就会调用RecyclerViewdispatchTouchEvent方法。

AppBarLayout的事件拦截逻辑上文已经分析了。现在看RecyclerView的事件分析。

此时遍历RecyclerView,前提是点击区域在RecyclerView的内部,走它的dispatchTouchEvent方法,由于RecyclerView没有重写dispatchTouchEvent方法,所以走的依然是ViewGroupdispatchTouchEvent方法。

如果ACTION_DOWN点击区域在RecyclerView内部,会走它的onInterceptTouchEvent方法。RecyclerView重写了onInterceptTouchEvent方法:

public boolean onInterceptTouchEvent(MotionEvent e) {
    ......
    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
    final boolean canScrollVertically = mLayout.canScrollVertically();

    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(e);

    final int action = e.getActionMasked();
    final int actionIndex = e.getActionIndex();

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            if (mIgnoreMotionEventTillDown) {
                mIgnoreMotionEventTillDown = false;
            }
            mScrollPointerId = e.getPointerId(0);
            mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

            if (mScrollState == SCROLL_STATE_SETTLING) {
                getParent().requestDisallowInterceptTouchEvent(true);
                setScrollState(SCROLL_STATE_DRAGGING);
                stopNestedScroll(TYPE_NON_TOUCH);
            }

            // Clear the nested offsets
            mNestedOffsets[0] = mNestedOffsets[1] = 0;

            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontally) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertically) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            break;
		......
        case MotionEvent.ACTION_UP: {
            mVelocityTracker.clear();
            stopNestedScroll(TYPE_TOUCH);
        } break;
		......
    }
    return mScrollState == SCROLL_STATE_DRAGGING;
}

先看RecyclerViewonInterceptTouchEvent方法的返回值,如果mScrollState = SCROLL_STATE_DRAGGING,返回值为true。上面代码中mScrollState = SCROLL_STATE_SETTLING的情况下,才会设置mScrollState = SCROLL_STATE_DRAGGING。我们知道对于RecyclerView它丝毫没动的情况下,mScrollState=SCROLL_STATE_IDLE的。所以对于ACTION_DOWN事件,RecyclerViewonInterceptTouchEvent方法的返回值为false。

什么情况下mScrollState = SCROLL_STATE_SETTLING呢?这个状态是代码中RecyclerView进行滑动,比如Fling操作的时候,RecyclerView的mScrollState = SCROLL_STATE_SETTLING,此时还没有结束Fling滑动的话,此时手指按下,RecyclerViewonInterceptTouchEvent方法的返回值为true了。

回头继续分析RecyclerViewonInterceptTouchEvent方法返回值在ACTION_DOWN事件时返回false。此时就会到了RecyclerViewdispatchTouchEvent方法了,遍历RecyclerView的各个子View的情况我们先不考虑,就会走RecyclerViewonTouchEvent方法。

5.8 RecyclerView的onTouchEvent方法

public boolean onTouchEvent(MotionEvent e) {
    ......
    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
    final boolean canScrollVertically = mLayout.canScrollVertically();

    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    boolean eventAddedToVelocityTracker = false;

    final int action = e.getActionMasked();
    final int actionIndex = e.getActionIndex();

    if (action == MotionEvent.ACTION_DOWN) {
        mNestedOffsets[0] = mNestedOffsets[1] = 0;
    }
    final MotionEvent vtev = MotionEvent.obtain(e);
    vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            mScrollPointerId = e.getPointerId(0);
            mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontally) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertically) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
        } break;
		.......
    }

    if (!eventAddedToVelocityTracker) {
        mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();

    return true;
}

RecyclerViewonTouchEvent方法可以看出,返回值为true,说明只要点击区域在RecyclerView区域内部,默认情况下RecyclerView是拦截事件的。

至此,我们总结一下RecyclerView对于ACTION_DOWN的事件处理。
首先CoordinatorLayoutdispatchTouchEvent方法接收到ACTION_DOWN事件,走它的onInterceptTouchEvent方法,在这个方法中,分别调用AppBarLayout.BehaviorAppBarLayout.ScrollingViewBehavioronInterceptTouchEvent方法,都返回了false。因为对于AppBarLayout.Behavior来说,点击区域在RecyclerView上,所以它返回了false。对于AppBarLayout.ScrollingViewBehavior来说,没有重写父类的onInterceptTouchEvent方法,默认返回false。

此时事件就回到了CoordinatorLayoutdispatchTouchEvent方法,遍历各个子View,因为ACTION_DOWN事件在RecyclerView的区域内,就会调用RecyclerView的dispatchTouchEvent方法,先走RecyclerViewonInterceptTouchEvent方法,通常情况下该方法返回false。对于RecyclerViewdispatchTouchEvent方法经历遍历各个子View,没有找到处理事件的子View,就会走自己的onTouchEvent方法,默认情况下RecyclerViewonTouchEvent方法返回true。紧接着返回值向上追溯,就会到CoordinatorLayoutdispatchTouchEvent方法遍历子View找到了处理事件的子View。那么后续的ACTION_MOVE、ACTION_UP事件,就会交给RecyclerView进行处理。

在这里插入图片描述

可以看出RecyclerView的事件拦截处理非常常规,跟Behavior关系不大。
RecyclerView拦截了ACTION_DOWN事件后,后续的ACTION_MOVE和ACTION_UP自然就交给RecyclerViewonTouchEvent方法来处理,自然就可以滑动起来。

5.9 小结

到目前为止,我们分析了AppBarLayout如何滑动起来的和RecyclerView如何滑动起来的问题,已经说明完毕了,可以看出这两个组件在拦截事件处理滑动上,逻辑是不同的。

剩下的问题就是AppBarLayout滑动起来的时候,如何让RecyclerView跟着联动滑动的问题,和RecyclerView滑动的时候AppBarLayout如何联动滑动的问题了。这两个逻辑依然是不同的。

5.10 AppBarLayout的协调滑动

上文分析AppBarLayout的滑动逻辑是在AppBarLayout.Behavior中的onTouchEvent方法中进行的。

public boolean onTouchEvent(
      @NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
    boolean consumeUp = false;
    switch (ev.getActionMasked()) {
      case MotionEvent.ACTION_MOVE:
        final int activePointerIndex = ev.findPointerIndex(activePointerId);
        if (activePointerIndex == -1) {
          return false;
        }

        final int y = (int) ev.getY(activePointerIndex);
        int dy = lastMotionY - y;
        lastMotionY = y;
        // We're being dragged so scroll the ABL
        scroll(parent, child, dy, getMaxDragOffset(child), 0);
        break;
      ......
    }

    if (velocityTracker != null) {
      velocityTracker.addMovement(ev);
    }

    return isBeingDragged || consumeUp;
  }

AppBarLayout.Behavior中的onTouchEvent方法中接收到ACTION_MOVE事件后,调用scroll方法处理滑动,具体如何滑动不详细分析。

问题是,AppBarLayout滑动的时候,RecyclerView如何进行联动的??

先把视线回到CoordinatorLayout类中,其源码如下:

public void onAttachedToWindow() {
    super.onAttachedToWindow();
    resetTouchBehaviors(false);
    if (mNeedsPreDrawListener) {
        if (mOnPreDrawListener == null) {
            mOnPreDrawListener = new OnPreDrawListener();
        }
        final ViewTreeObserver vto = getViewTreeObserver();
        vto.addOnPreDrawListener(mOnPreDrawListener);
    }
    if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
        // We're set to fitSystemWindows but we haven't had any insets yet...
        // We should request a new dispatch of window insets
        ViewCompat.requestApplyInsets(this);
    }
    mIsAttachedToWindow = true;
}

public void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    resetTouchBehaviors(false);
    if (mNeedsPreDrawListener && mOnPreDrawListener != null) {
        final ViewTreeObserver vto = getViewTreeObserver();
        vto.removeOnPreDrawListener(mOnPreDrawListener);
    }
    if (mNestedScrollingTarget != null) {
        onStopNestedScroll(mNestedScrollingTarget);
    }
    mIsAttachedToWindow = false;
}

CoordinatorLayout类中的onAttachedToWindow方法和onDetachedFromWindow方法中分别注册和删除一个监听器OnPreDrawListener

继续看监听器的代码:

class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
    @Override
    public boolean onPreDraw() {
        onChildViewsChanged(EVENT_PRE_DRAW);
        return true;
    }
}

这个监听器很关键,代表的意思是在View树发生变化时,调用这个监听器的方法。

再看onChildViewsChanged方法的源码:

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
   final int layoutDirection = ViewCompat.getLayoutDirection(this);
   final int childCount = mDependencySortedChildren.size();
   final Rect inset = acquireTempRect();
   final Rect drawRect = acquireTempRect();
   final Rect lastDrawRect = acquireTempRect();

   for (int i = 0; i < childCount; i++) {
       final View child = mDependencySortedChildren.get(i);
       final LayoutParams lp = (LayoutParams) child.getLayoutParams();
       if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
           // Do not try to update GONE child views in pre draw updates.
           continue;
       }
	   .......
       for (int j = i + 1; j < childCount; j++) {
           final View checkChild = mDependencySortedChildren.get(j);
           final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
           final Behavior b = checkLp.getBehavior();

           if (b != null && b.layoutDependsOn(this, checkChild, child)) {
               if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                   checkLp.resetChangedAfterNestedScroll();
                   continue;
               }

               final boolean handled;
               switch (type) {
                   case EVENT_VIEW_REMOVED:
                       // EVENT_VIEW_REMOVED means that we need to dispatch
                       // onDependentViewRemoved() instead
                       b.onDependentViewRemoved(this, checkChild, child);
                       handled = true;
                       break;
                   default:
                       // Otherwise we dispatch onDependentViewChanged()
                       handled = b.onDependentViewChanged(this, checkChild, child);
                       break;
               }
				......
           }
       }
   }

   releaseTempRect(inset);
   releaseTempRect(drawRect);
   releaseTempRect(lastDrawRect);
}

onChildViewsChanged方法内部,遍历各个子View,调用了子View的Behavior对象的layoutDependsOn方法,上文已经说明RecyclerViewBehaviorAppBarLayout.ScrollgingViewBehavior,在AppBarLayout.ScrollgingViewBehavior类中有如下源码:

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
 // We depend on any AppBarLayouts
 return dependency instanceof AppBarLayout;
}

这行代码就是告诉CoordiantorLayout组件,RecyclerViewBehavior是依赖于AppBarLayout组件的。如果AppBarLayout组件布局变化了,告诉RecyclerView,然后RecyclerView就知道了。然后AppBarLayout.ScrollgingViewBehavioronDependentViewChanged方法就接着被调用。

AppBarLayout.ScrollgingViewBehavior源码:

@Override
public boolean onDependentViewChanged(
    @NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
  offsetChildAsNeeded(child, dependency);
  updateLiftedStateIfNeeded(child, dependency);
  return false;
}

private void offsetChildAsNeeded(@NonNull View child, @NonNull View dependency) {
  final CoordinatorLayout.Behavior behavior =
      ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
  if (behavior instanceof BaseBehavior) {
    // Offset the child, pinning it to the bottom the header-dependency, maintaining
    // any vertical gap and overlap
    final BaseBehavior ablBehavior = (BaseBehavior) behavior;
    ViewCompat.offsetTopAndBottom(
        child,
        (dependency.getBottom() - child.getTop())
            + ablBehavior.offsetDelta
            + getVerticalLayoutGap()
            - getOverlapPixelsForOffset(dependency));
  }
}

这样一来,RecyclerView就跟着AppBarLayout协调滑动了。

5.11 RecycleVIew的协调滑动

上文分析到RecycleVIew的滑动事件处理逻辑是在它的onTouchEvent方法中进行的。
其源码如下:

public boolean onTouchEvent(MotionEvent e) {
        ......
        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
        final boolean canScrollVertically = mLayout.canScrollVertically();

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        boolean eventAddedToVelocityTracker = false;

        final int action = e.getActionMasked();
        final int actionIndex = e.getActionIndex();

        if (action == MotionEvent.ACTION_DOWN) {
            mNestedOffsets[0] = mNestedOffsets[1] = 0;
        }
        final MotionEvent vtev = MotionEvent.obtain(e);
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mScrollPointerId = e.getPointerId(0);
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            } break;
            ......
            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id "
                            + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;

                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally) {
                        if (dx > 0) {
                            dx = Math.max(0, dx - mTouchSlop);
                        } else {
                            dx = Math.min(0, dx + mTouchSlop);
                        }
                        if (dx != 0) {
                            startScroll = true;
                        }
                    }
                    if (canScrollVertically) {
                        if (dy > 0) {
                            dy = Math.max(0, dy - mTouchSlop);
                        } else {
                            dy = Math.min(0, dy + mTouchSlop);
                        }
                        if (dy != 0) {
                            startScroll = true;
                        }
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mReusableIntPair[0] = 0;
                    mReusableIntPair[1] = 0;
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        dx -= mReusableIntPair[0];
                        dy -= mReusableIntPair[1];
                        // Updated the nested offsets
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        // Scroll has initiated, prevent parents from intercepting
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }

                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];

                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            e)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

            
            case MotionEvent.ACTION_UP: {
                ......
                resetScroll();
            } break;
			......
        }

        if (!eventAddedToVelocityTracker) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();

        return true;
    }

首先RecyclerView接收到ACTION_DOWN事件后,调用了startNestedScroll(nestedScrollAxis, TYPE_TOUCH);

这个方法源码如下:

@Override
public boolean startNestedScroll(int axes, int type) {
    return getScrollingChildHelper().startNestedScroll(axes, type);
}

其中getScrollingChildHelper()方法返回的是NestedScrollingChildHelper对象。

再看RecyclerView接收到ACTION_MOVE事件后,

dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )

方法被调用。
dispatchNestedPreScroll方法的源码如下:

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
        int type) {
    return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
            type);
}

同样走到NestedScrollingChildHelper对象的dispatchNestedPreScroll方法中。

下面详细了解类NestedScrollingChildHelper

5.11.1 NestedScrollingChildHelper类

public NestedScrollingChildHelper(@NonNull View view) {
    mView = view;
}

构造器参数的View就是需要支持嵌套滑动的子View。比如在RecyclerView中创建的NestedScrollingChildHelper对象,这个参数View就是RecyclerView对象实例。

1) 方法setNestedScrollingEnabled

public void setNestedScrollingEnabled(boolean enabled) {
    if (mIsNestedScrollingEnabled) {
        ViewCompat.stopNestedScroll(mView);
    }
    mIsNestedScrollingEnabled = enabled;
}

该方法是设置mView是否支持嵌套滑动。对于RecyclerView来讲,默认是支持的。从RecyclerView的代码中就可以知道。如下:

        boolean nestedScrollingEnabled = true;
        if (Build.VERSION.SDK_INT >= 21) {
            a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS,
                    defStyleAttr, 0);
            if (Build.VERSION.SDK_INT >= 29) {
                saveAttributeDataForStyleable(
                        context, NESTED_SCROLLING_ATTRS, attrs, a, defStyleAttr, 0);
            }
            nestedScrollingEnabled = a.getBoolean(0, true);
            a.recycle();
        }
        // Re-set whether nested scrolling is enabled so that it is set on all API levels
        setNestedScrollingEnabled(nestedScrollingEnabled);

这是RecyclerView中的代码。nestedScrollingEnabled的默认值是true,并且从XML属性中解析的值默认也是true。

2) 方法startNestedScroll

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    setNestedScrollingParentForType(type, p);
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

1 如果已经找到支持嵌套滑动的parentView,第一个if语句中直接返回true。
2 如果没有找到,进入第二个if语句。
3 如果mView自己不支持嵌套滑动,直接返回false。
4 如果mView自己支持嵌套滑动,就进入第二个if语句的逻辑。循环往上一层一层找支持嵌套滑动的parentView
5 如果遍历完毕都没有找到,直接返回false。
6 如果找到了一个,直接停止遍历,返回true。同时调用setNestedScrollingParentForType(type, p);方法设置对应的类型的p。后续的逻辑直接使用找到的p

需要说明的是
1 最终找的parentView满足的条件是:它的onStartNestedScroll方法返回true即可。
2 参数说明最终找到的参数说明:
p:就是最终结束遍历的parentView。它的onStartNestedScroll方法返回true
child:就是mView的父view。可能是多级的父View。也可能是自己。但child肯定是p的直接child。这一点通过上面的循环遍历就可以得出。
mView:这个值一直没有变化,以RecyclerView为例,这个值就是RecyclerView

3) 方法dispatchNestedPreScroll

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    consumed = getTempNestedScrollConsumed();
                }
                consumed[0] = 0;
                consumed[1] = 0;
                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

该方法解析:
1 如果mView自己不支持嵌套滑动,直接返回false。
2 mView自己支持嵌套滑动,直接根据类型获取p
3 如果p没有获取到,直接返回false。也就是说没有父View可以和mView直接嵌套滑动
4 如果p有获取到,说明p想要和mView直接进行嵌套滑动。此时就会判断dxdy的值
5 如果dx=0且dy=0,说明没有滑动距离.只有当dxdy只要有一个不等于0即可。
具体dx=0还是dy=0要看mView的设置。

RecyclerView来举例。
如果RecyclerView设置的垂直滑动,不能水平滑动,该方法的dx必定=0的。
如果RecyclerView设置的水平滑动,不能垂直滑动,该方法的dy必定=0的。
如果RecyclerView同时支持水平滑动和垂直滑动,该方法的dxdy都可能不等于0。

offsetInWindow这个参数是个两个元素的数组,具体是保存mView在整个window界面的位置的。初始值为0。
consumed这个值也是两个元素的数组,初始值为0。这个值的目的是传递给p之后,设置p消耗的距离。
dxdy代表触发的滑动距离,也就是p这一次能够最大滑动的距离,consumed代表实际消耗的距离,如果p消耗了全部可滑动的距离,那么consumed的值与dxdy的值是相等的。

所以这两行代码

consumed[0] = 0;
consumed[1] = 0;

意思是初始化p消耗的距离,让p设置这两个值,让mView能够感知到p消耗的距离。
接下来重要的是

ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

这一行代码把滑动距离交给了parent处理,并且parent把自己消耗的距离通过初始值为0的两个元素的数组consumed来告诉mView

处理完毕之后,重新计算mView在Window窗口中的位置,计算偏移量,并保存在offsetInWindow数组中。
最后,根据consumed的值来决定返回true/false。只要parent消耗了距离,就返回true,否则就返回false,代表parent没有消耗距离。

4) 方法dispatchNestedScroll

public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type,
            @Nullable int[] consumed) {
        dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                offsetInWindow, type, consumed);
    }

    private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type, @Nullable int[] consumed) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    consumed = getTempNestedScrollConsumed();
                    consumed[0] = 0;
                    consumed[1] = 0;
                }

                ViewParentCompat.onNestedScroll(parent, mView,
                        dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return true;
            } else if (offsetInWindow != null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

该方法解析:
1 如果mView不支持嵌套滑动,直接返回false
2 如果mView支持嵌套滑动,就尝试获取p
3 如果p没有获取到,就直接返回false
4 如果p获取到,则有交给p处理,调用ponNestedScroll方法,同时p处理后的距离通过consumed两个元素的数组返回。

5) 方法stopNestedScroll

public void stopNestedScroll(@NestedScrollType int type) {
    ViewParent parent = getNestedScrollingParentForType(type);
    if (parent != null) {
        ViewParentCompat.onStopNestedScroll(parent, mView, type);
        setNestedScrollingParentForType(type, null);
    }
}

停止嵌套滑动是,直接获取p,获取到p之后,直接调用ponStopNestedScroll方法。同时将mView对应的p对象设置为null
等到下次再次嵌套滑动时,重新获取p

5.11.2 嵌套滑动逻辑

RecyclerView接收到ACTION_DOWN事件后,调用startNestedScroll方法,走到NestedScrollingChildHelper对象的startNestedScroll方法。这个方法内部递归往上找父View,源码如下:

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    if (hasNestedScrollingParent(type)) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                setNestedScrollingParentForType(type, p);
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

如果父View的onStartNestedScroll方法返回true,就设置找到了p处理嵌套滑动。
对于文章开头的demo来说,这个p就是CooridnatorLayout组件。

直接看CooridnatorLayoutonStartNestedScroll方法:

public boolean onStartNestedScroll(View child, View target, int axes, int type) {
    boolean handled = false;

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        if (view.getVisibility() == View.GONE) {
            // If it's GONE, don't dispatch
            continue;
        }
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                    target, axes, type);
            handled |= accepted;
            lp.setNestedScrollAccepted(type, accepted);
        } else {
            lp.setNestedScrollAccepted(type, false);
        }
    }
    return handled;
}

CooridnatorLayoutonStartNestedScroll方法可以看出,它简单的遍历了子View,如果子View的BehavioronStartNestedScroll方法返回true,自己的onStartNestedScroll方法就返回true。

紧接着就走到了AppBarLayoutBehavior,也就是AppBarLayout.BehavoironStartNestedScroll方法:

public boolean onStartNestedScroll(
    @NonNull CoordinatorLayout parent,
    @NonNull T child,
    @NonNull View directTargetChild,
    View target,
    int nestedScrollAxes,
    int type) {
  final boolean started =
      (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
          && (child.isLiftOnScroll() || canScrollChildren(parent, child, directTargetChild));
	......
  return started;
}

可以看出,AppBarLayout.BehavoironStartNestedScroll方法主要是计算AppBarLayout是否能够上下滑动作为返回值的。
如果不是上下滑动,AppBarLayout.BehavoironStartNestedScroll方法就返回false了。
如果 AppBarLayout没有滑出屏幕外面,并且是上下滑动,那么started=true

往回追溯,CooridnatorLayoutonStartNestedScroll方法返回true,再追溯到NestedScrollingChildHelper对象的startNestedScroll方法找到了p

这样ACTION_DOWN事件的嵌套处理逻辑已经完成,紧接着RecyclerViewonTouchEvent方法处理ACTION_MOVE事件。它的dispatchNestedPreScroll方法被调用,接着走到NestedScrollingChildHelper对象的dispatchNestedPreScroll方法。

NestedScrollingChildHelper对象的dispatchNestedPreScroll方法源码:

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
       @Nullable int[] offsetInWindow, @NestedScrollType int type) {
   if (isNestedScrollingEnabled()) {
       final ViewParent parent = getNestedScrollingParentForType(type);
       if (parent == null) {
           return false;
       }

       if (dx != 0 || dy != 0) {
           ......
           ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

           ......
           return consumed[0] != 0 || consumed[1] != 0;
       } else if (offsetInWindow != null) {
           offsetInWindow[0] = 0;
           offsetInWindow[1] = 0;
       }
   }
   return false;
}

上面onStartNestedScroll方法中已经找到了p,也就是CoordiantorLayout,所以这里的parent不为空,parent=CoordiantorLayout,紧接着在dispatchNestedPreScroll方法内会调用CoordiantorLayout类的onNestedPreScroll方法。

dispatchNestedPreScroll方法的返回值consumed[0] != 0 || consumed[1] != 0,这两个值代表的意思是如果CoordiantorLayout中的onNestedPreScroll方法消耗了滑动距离,就把CoordiantorLayout消耗的滑动距离设置到consumed[0] 或者consumed[1]中。如果是水平方向消耗就是consumed[0]的值,如果是垂直方向消耗就是consumed[1]的值。

下面看CoordiantorLayoutonNestedPreScroll方法源码:

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int  type) {
    int xConsumed = 0;
    int yConsumed = 0;
    boolean accepted = false;

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        ......
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
         if (!lp.isNestedScrollAccepted(type)) {
             continue;
         }
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            mBehaviorConsumed[0] = 0;
            mBehaviorConsumed[1] = 0;
            viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mBehaviorConsumed, type);

            xConsumed = dx > 0 ? Math.max(xConsumed, mBehaviorConsumed[0])
                    : Math.min(xConsumed, mBehaviorConsumed[0]);
            yConsumed = dy > 0 ? Math.max(yConsumed, mBehaviorConsumed[1])
                    : Math.min(yConsumed, mBehaviorConsumed[1]);

            accepted = true;
        }
    }

    consumed[0] = xConsumed;
    consumed[1] = yConsumed;

    if (accepted) {
        onChildViewsChanged(EVENT_NESTED_SCROLL);
    }
}

该方法内部同样遍历子View的Behavior,分别调用BehavioronNestedPreScroll方法,并把自己的消耗距离设置到consumed[0]consumed[1],最后accepted=true的情况下,调用onChildViewsChanged方法。

CoordiantorLayoutonNestedPreScroll方法内部,会调用各个子View的Behavior
onNestedPreScroll方法。

也就是AppBarLayoutBehavior中的onNestedPreScroll方法,其源码如下:

public void onNestedPreScroll(
    CoordinatorLayout coordinatorLayout,
    @NonNull T child,
    View target,
    int dx,
    int dy,
    int[] consumed,
    int type) {
  if (dy != 0) {
    int min;
    int max;
    if (dy < 0) {
      // We're scrolling down
      min = -child.getTotalScrollRange();
      max = min + child.getDownNestedPreScrollRange();
    } else {
      // We're scrolling up
      min = -child.getUpNestedPreScrollRange();
      max = 0;
    }
    if (min != max) {
      consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
    }
  }
  if (child.isLiftOnScroll()) {
    child.setLiftedState(child.shouldLift(target));
  }
}

AppBarLayoutBehavior中的onNestedPreScroll方法在处理向下滑动和向上滑动的逻辑是不一样的。

min != max的情况下,才会在onNestedPreScroll方法中消耗距离。
当手指向上滑动的时候,dy>0,此时minmax值是不等的,scroll方法才会调用。
当手指向下滑动的时候,dy<0,此时minmax值相等,scroll方法不会调用。

向上滑动的时候,日志如下:
在这里插入图片描述

向下滑动的时候,日志如下:
在这里插入图片描述

AppBarLayoutBehavior中的onNestedPreScroll方法在处理向下滑动和向上滑动的逻辑是
当手指向上滑动的时候,dy>0,此时minmax值是不等的,scroll方法才会调用。此时AppBarLayout的布局已经改变,接着在CoordiantorLayoutonNestedPreScroll方法中的onChildViewsChanged方法被调用,接着ScrollingViewBehavioronDependViewChanged方法就会被调用,然后RecyclerVIew就跟着嵌套滑动了。

当手指向下滑动的时候,dy<0,此时minmax值相等,scroll方法不会调用。AppBarLayout的布局没有改变,接着在CoordiantorLayoutonNestedPreScroll方法中的onChildViewsChanged方法被调用,但是AppBarLayout的布局没有改变,所以ScrollingViewBehavioronDependViewChanged方法就不会被调用。

向上追溯代码,回到NestedScrollingChildHelper中的dispatchNestedPreScroll方法,它的dispatchNestedPreScroll方法的返回值如果有消耗距离consumed[0] != 0 || consumed[1] != 0,返回值就为true。

在向上追溯代码,回到RecyclerViewonTouchEvent方法中,

public boolean onTouchEvent(MotionEvent e) {
     ......
     final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
     final boolean canScrollVertically = mLayout.canScrollVertically();

     if (mVelocityTracker == null) {
         mVelocityTracker = VelocityTracker.obtain();
     }
     boolean eventAddedToVelocityTracker = false;

     final int action = e.getActionMasked();
     final int actionIndex = e.getActionIndex();

     final MotionEvent vtev = MotionEvent.obtain(e);
     vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

     switch (action) {
	     ......

         case MotionEvent.ACTION_MOVE: {
             ......
             final int x = (int) (e.getX(index) + 0.5f);
             final int y = (int) (e.getY(index) + 0.5f);
             int dx = mLastTouchX - x;
             int dy = mLastTouchY - y;
			 .......
             if (mScrollState == SCROLL_STATE_DRAGGING) {
                 mReusableIntPair[0] = 0;
                 mReusableIntPair[1] = 0;
                 if (dispatchNestedPreScroll(
                         canScrollHorizontally ? dx : 0,
                         canScrollVertically ? dy : 0,
                         mReusableIntPair, mScrollOffset, TYPE_TOUCH
                 )) {
                     dx -= mReusableIntPair[0];
                     dy -= mReusableIntPair[1];
                     // Updated the nested offsets
                     mNestedOffsets[0] += mScrollOffset[0];
                     mNestedOffsets[1] += mScrollOffset[1];
                     // Scroll has initiated, prevent parents from intercepting
                     getParent().requestDisallowInterceptTouchEvent(true);
                 }

                 mLastTouchX = x - mScrollOffset[0];
                 mLastTouchY = y - mScrollOffset[1];

                 if (scrollByInternal(
                         canScrollHorizontally ? dx : 0,
                         canScrollVertically ? dy : 0,
                         e)) {
                     getParent().requestDisallowInterceptTouchEvent(true);
                 }
                 if (mGapWorker != null && (dx != 0 || dy != 0)) {
                     mGapWorker.postFromTraversal(this, dx, dy);
                 }
             }
         } break;
		......
}

     if (!eventAddedToVelocityTracker) {
         mVelocityTracker.addMovement(vtev);
     }
     vtev.recycle();

     return true;
 }

上面的代码中,dispatchNestedPreScroll方法如果消耗了距离,返回值为true,就会走if语句里面,dx -= mReusableIntPair[0]; dy -= mReusableIntPair[1];着两行代码就会生效,把消耗的距离减去。如果没有消耗距离,if语句的返回值为false,就不减。

紧接着就会走scrollByInternal方法。

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int unconsumedX = 0;
    int unconsumedY = 0;
    int consumedX = 0;
    int consumedY = 0;

    consumePendingUpdateOperations();
    if (mAdapter != null) {
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        scrollStep(x, y, mReusableIntPair);
        consumedX = mReusableIntPair[0];
        consumedY = mReusableIntPair[1];
        unconsumedX = x - consumedX;
        unconsumedY = y - consumedY;
    }
    if (!mItemDecorations.isEmpty()) {
        invalidate();
    }

    mReusableIntPair[0] = 0;
    mReusableIntPair[1] = 0;
    dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
            TYPE_TOUCH, mReusableIntPair);
    unconsumedX -= mReusableIntPair[0];
    unconsumedY -= mReusableIntPair[1];
    boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;

    // Update the last touch co-ords, taking any scroll offset into account
    mLastTouchX -= mScrollOffset[0];
    mLastTouchY -= mScrollOffset[1];
    mNestedOffsets[0] += mScrollOffset[0];
    mNestedOffsets[1] += mScrollOffset[1];

    if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
        if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
            pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
        }
        considerReleasingGlowsOnScroll(x, y);
    }
    if (consumedX != 0 || consumedY != 0) {
        dispatchOnScrolled(consumedX, consumedY);
    }
    if (!awakenScrollBars()) {
        invalidate();
    }
    return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}

这个方法内部,调用了dispatchNestedScroll方法,该方法源码:

public final void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, int[] offsetInWindow, int type, @NonNull int[] consumed) {
        getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
    }

继续向下走NestedScrollingChildHelperdispatchNestedScroll方法,该方法就会走CoordinatorLayoutonNestedScroll方法,其源码如下:

public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @ViewCompat.NestedScrollType int type,
            @NonNull int[] consumed) {
    final int childCount = getChildCount();
    boolean accepted = false;
    int xConsumed = 0;
    int yConsumed = 0;

    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        ......
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        if (!lp.isNestedScrollAccepted(type)) {
            continue;
        }

        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {

            mBehaviorConsumed[0] = 0;
            mBehaviorConsumed[1] = 0;

            viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
                    dxUnconsumed, dyUnconsumed, type, mBehaviorConsumed);

            xConsumed = dxUnconsumed > 0 ? Math.max(xConsumed, mBehaviorConsumed[0])
                    : Math.min(xConsumed, mBehaviorConsumed[0]);
            yConsumed = dyUnconsumed > 0 ? Math.max(yConsumed, mBehaviorConsumed[1])
                    : Math.min(yConsumed, mBehaviorConsumed[1]);

            accepted = true;
        }
    }

    consumed[0] += xConsumed;
    consumed[1] += yConsumed;

    if (accepted) {
        onChildViewsChanged(EVENT_NESTED_SCROLL);
    }
}

非常相似的逻辑,遍历子View,分别调用各个子View的BehavioronNestedScroll方法,最后调用onChildViewsChanged方法。如果AppBarLayout的布局变化了,就通过遍历子View的onDependViewChanged方法通知RecyclerView进行嵌套滑动。

下面看AppBarLayout.BehavioronNestedScroll方法源码:

public void onNestedScroll(
        CoordinatorLayout coordinatorLayout,
        @NonNull T child,
        View target,
        int dxConsumed,
        int dyConsumed,
        int dxUnconsumed,
        int dyUnconsumed,
        int type,
        int[] consumed) {
  if (dyUnconsumed < 0) {
    // If the scrolling view is scrolling down but not consuming, it's probably be at
    // the top of it's content
    consumed[1] =
        scroll(coordinatorLayout, child, dyUnconsumed, -child.getDownNestedScrollRange(), 0);
  }

  if (dyUnconsumed == 0) {
    // The scrolling view may scroll to the top of its content without updating the actions, so
    // update here.
    updateAccessibilityActions(coordinatorLayout, child);
  }
}

dyUnconsumed < 0 的时候,onNestedPreScroll没处理,然后再onNestedScroll方法中进行了处理。然后回到CoordinatorLayoutonNestedScroll方法中,调用onChildViewsChanged通知RecyclerView进行嵌套滑动。

5.11.3 小结

在这里插入图片描述

六 讨论

6.1

Behavior在嵌套滑动中起着什么样的作用?CoordinatorLayout组件又有什么作用?

Behavior在嵌套滑动中作用相当于粘合剂的作用。各个View实现各个的Behavior,具体Behavior实现自己的逻辑,但是Behavior的逻辑的相互之间的逻辑实现,是通过CoordinatorLayout作为中间层实现的,起到中间转发的作用。例如Demo中的AppBarLayout中的滑动,RecyclerView要嵌套滑动就是通过CoordinatorLayout中的监听器OnPreDrawListener的方法中调用RecyclerViewBehavioronDependedViewChanged方法实现的。再看RecyclerView滑动的是时候,AppBarLayoutBehavior中的方法onStartNestedScrollonNestedScrollAcceptedonNestedPreScrollonNestedScroll等等方法,这样RecyclerView处理滑动的时候,AppBarLayout也有机会处理滑动,达到嵌套滑动的目的。

6.2

协调布局自身抖动的Bug是什么原因产生的?如何解决?

具体原因文中已有说明。具体Bug解决如下:

class FixBehavior : AppBarLayout.ScrollingViewBehavior {
  constructor() : super()
  constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)

  override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: View, directTargetChild: View, target: View, axes: Int): Boolean {
      return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes)
  }

  override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: View, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
      stopAppBarLayoutScroller(coordinatorLayout)
      return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type)
  }

  private fun stopAppBarLayoutScroller(coordinatorLayout: CoordinatorLayout) {
      try {
          val appBarView = coordinatorLayout.getChildAt(0) as AppBarLayout
          val appBarLp = appBarView.layoutParams as CoordinatorLayout.LayoutParams
          if (appBarLp.behavior != null) {
              stopBehaviorScroller(appBarLp.behavior as AppBarLayout.Behavior)
          }
      } catch (e: Exception) {
          e.printStackTrace()
      }
  }

  private fun stopBehaviorScroller(appBarBehavior: AppBarLayout.Behavior) {
      try {
          val filed = appBarBehavior.javaClass.superclass?.superclass?.getDeclaredField("scroller")
          if (filed != null) {
              filed.isAccessible = true
              val headerBehaviorScroller = filed.get(appBarBehavior)
              if (headerBehaviorScroller != null
                      && headerBehaviorScroller is OverScroller
                      && !headerBehaviorScroller.isFinished) {
                  headerBehaviorScroller.abortAnimation()
              }
          }
      } catch (e: Exception) {
          e.printStackTrace()
      }
  }
}

具体解决方法不止这一个,肯定有其他更好的方法。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值