协调布局

编程入门 行业动态 更新时间:2024-10-06 01:44:04

协调<a href=https://www.elefans.com/category/jswz/34/1770549.html style=布局"/>

协调布局

一 协调布局示例

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

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=""xmlns:app=""android:layout_width="match_parent"android:layout_height="match_parent"><com.google.android.material.appbar.AppBarLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><androidx.appcompat.widget.AppCompatImageViewandroid: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.RecyclerViewandroid: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 annotationClass<?> 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 draggedif (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 ABLscroll(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);velocityTrackerputeCurrentVelocity(1000);float yvel = velocityTracker.getYVelocity(activePointerId);fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);}// $FALLTHROUGHcase 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 offsetsmNestedOffsets[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 ABLscroll(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 insetsViewCompat.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 {@Overridepublic 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() insteadb.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 AppBarLayoutsreturn 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 overlapfinal 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 offsetsmNestedOffsets[0] += mScrollOffset[0];mNestedOffsets[1] += mScrollOffset[1];// Scroll has initiated, prevent parents from interceptinggetParent().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 levelssetNestedScrollingEnabled(nestedScrollingEnabled);

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

2) 方法startNestedScroll

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {if (hasNestedScrollingParent(type)) {// Already in progressreturn 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 progressreturn 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 dispatchcontinue;}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 downmin = -child.getTotalScrollRange();max = min + child.getDownNestedPreScrollRange();} else {// We're scrolling upmin = -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 offsetsmNestedOffsets[0] += mScrollOffset[0];mNestedOffsets[1] += mScrollOffset[1];// Scroll has initiated, prevent parents from interceptinggetParent().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 accountmLastTouchX -= 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 contentconsumed[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 AppBarLayoutval appBarLp = appBarView.layoutParams as CoordinatorLayout.LayoutParamsif (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 = trueval headerBehaviorScroller = filed.get(appBarBehavior)if (headerBehaviorScroller != null&& headerBehaviorScroller is OverScroller&& !headerBehaviorScroller.isFinished) {headerBehaviorScroller.abortAnimation()}}} catch (e: Exception) {e.printStackTrace()}}
}

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

更多推荐

协调布局

本文发布于:2024-02-28 03:43:59,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1767821.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:布局

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!