一 协调布局示例
从最简单的协调布局嵌套滑动开始,首先看最简单的协调布局。
最外层一个CoordinatorLayout
布局,它的子View只有AppBarLayout
和RecyclerView
,这就实现了最简单的协调布局。具体布局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
。在CoordinatorLayout
。LayoutParams
的构造方法中,源码如下:
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);
}
}
从上面的代码可以看出,在创建RecyclerView
的LayoutParams
对象时,会解析布局文件中设置的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();
就是设置AppBarLayout
的Behavior
的,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,要拦截事件,那么CoordinatorLayout
的onInterceptTouchEvent
方法就返回true。而对于上文中示例代码来说,CoordinatorLayout
组件内部只有两个子View,所以performIntercept
方法中遍历子View,调用对应子View的Behavior就是调用AppBarLayout
的AppBarLayout.Behavior
和RecyclerView
的AppBarLayout.ScrollingViewBehavior
。这一点需要明确,也非常关键。如果不理解的话,文章开头的部分已经说明。
有了以上的知识储备,现在回头总结到目前分析的源码,ACTION_DOWN事件的处理逻辑:
CoordinatorLayout
组件首先接收到ACTION_DOWN事件,走它的dispatchTouchEvent
方法,该方法CoordinatorLayout
组件没有重写,走的是ViewGroup
的dispatchTouchEvent
方法,该方法首先会调用onInterceptTouchEvent
方法,这个方法CoordinatorLayout
组件进行了重写,在onInterceptTouchEvent
方法内部调用了performIntercept
方法,循环遍历CoordinatorLayout
组件的各个子View的Behavior
。这样ACTION_DOWN事件就由CoordinatorLayout
组件传递给了子View。子View对应的Behavior
的onInterceptTouchEvent
方法判断是否需要拦截。
这里还有一点需要说明,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方法分析
对于RecyerView
的AppBarLayout.ScrollingViewBehavior
来说,没有重写父类的onInterceptTouchEvent
方法,直接使用的就是CoordinatorLayout
中的静态抽象类Behavior
的onInterceptTouchEvent
方法。
public static abstract class Behavior<V extends View> {
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull MotionEvent ev) {
return false;
}
}
至此,分析的逻辑用下图表示:
到目前为止,ACTION_DOWN事件回到了CoordinatorLayout
的dispatchTouchEvent
方法。也就是ViewGroup
的dispatchTouchEvent
方法。
CoordinatorLayout
的onInterceptTouchEvent
方法返回false
说明此时没有要拦截的View,此时根据ViewGroup
的dispatchTouchEvent
方法的逻辑,就是遍历子View。如果点击区域在AppBarLayout
的区域,就会调用AppBarLayout
的dispatchTouchEvent
方法。如果点击区域在RecyclerView
的区域,就会调用RecyclerView
的dispatchTouchEvent
方法。
此时出现了两种。这两种情况分开分析。
5.4 AppBarLayout事件拦截逻辑
首先遍历AppBarLayout
AppBarLayout
的源码中并没有重写dispatchTouchEvent
方法和onInterceptTouchEvent
方法,也就意味着即使点击区域在AppBarLayout
区域内部,此时它的onInterceptTouchEvent
方法依然返回false。
AppBarLayout
返回false的情况下,再遍历RecyclerView
。如果点击区域不在RecyclerView
内部,直接返回false。如果点击区域在RecyclerView
内部,接下来的第二种情况的分析才会有意义。
第二种情况先按下不说,继续来将点击区域在AppBarLayout
的区域内部,此时他的onInterceptTouchEvent
方法返回false,但是从表现上看,手指触摸在AppBarLayout
区域内确实能够滑动AppBarLayout
区域,这是怎么回事儿呢?既然AppBarLayout
的onInterceptTouchEvent
方法不拦截,那什么地方触发了事件拦截呢??
在Android事件分发机制中的分析,我们知道,在触摸区域在AppBarLayout
区域内的时候,RecyclerView
不会拦截事件,因为触摸区域不再RecyclerView
内部,又由于AppBarLayout
的onInterceptTouchEvent
方法返回false,并且AppBarLayout
自身没有重写onTouchEvent
方法,此时就跳出了循环遍历CoordinatorLayout
中的dispatchTouchEvent
方法遍历子View是否拦截的逻辑,并且没有找到拦截ACTION__DOWN事件的子View,根据Android事件分发机制中的分析,就会走到CoordinatorLayout
的onTouchEvent
方法的逻辑。
下面继续看CoordinatorLayout
的onTouchEvent
方法的源码
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;
}
删除了一些代码,CoordinatorLayout
的onTouchEvent
方法内部,首先调用performIntercept
方法,第二个参数注意是TYPE_ON_TOUCH值,如果这个方法返回true,代表有子View拦截事件,此时会获取这个子View的Behavior
,也就是mBehaviorTouchView
的值,然后调用该Behavior
的onTouchEvent
方法,该方法的返回值就是CoordinatorLayout
的onTouchEvent
方法的返回值。
对于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.Behavior
的onTouchEvent
方法分析
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.Behavior
的onInterceptTouchEvent
方法,在onInterceptTouchEvent
方法中,如果点击区域在AppBarLayout
内部,会设置isBeingDragged=true
。具体对应的代码如下:
isBeingDragged = canDragView(child) && parent.isPointInChildBounds(child, x, y);
这行代码就表明,如果点击区域在AppBarLayout
内部,即使AppBarLayout.Behavior
的onInterceptTouchEvent
方法返回了false,但是AppBarLayout.Behavior
的onTouchEvent
方法返回true。这样后续的事件就会交给AppBarLayout.Behavior
来处理。
具体说为什么后续事件交给AppBarLayout.Behavior
来处理的原因是什么呢?
因为分析到目前为止,点击区域在AppBarLayout
区域内部,此时由CoordinatorLayout
的onTouchEvent
方法内部,遍历子View,调用到了AppBarLayout.Behavior
的onTouchEvent
方法,这方法对于ACTION_DOWN事件返回了true,那对于CoordinatorLayout
的onTouchEvent
方法也就返回了true,继续往上追上CoordinatorLayout
的dispatchTouchEvent
方法返回了true。那么后续的事件ACTION_MOVE和ACTION_UP事件,就会交给CoordinatorLayout
的dispatchTouchEvent
方法,然后调用CoordinatorLayout
自己的onTouchEvent
方法,继续往下追溯到AppBarLayout.Behavior
的onTouchEvent
方法,而AppBarLayout.Behavior
的onTouchEvent
方法,对于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的事件拦截逻辑
上文我们分析了CoordinatorLayout
的onInterceptTouchEvent
方法,分了两种情况。
CoordinatorLayout
的onInterceptTouchEvent
方法返回false
说明此时没有要拦截的View,此时根据ViewGroup
的dispatchTouchEvent
方法的逻辑,就是遍历子View。如果点击区域在AppBarLayout
的区域,就会调用AppBarLayout
的dispatchTouchEvent
方法。如果点击区域在RecyclerView
的区域,就会调用RecyclerView
的dispatchTouchEvent
方法。
AppBarLayout
的事件拦截逻辑上文已经分析了。现在看RecyclerView
的事件分析。
此时遍历RecyclerView
,前提是点击区域在RecyclerView
的内部,走它的dispatchTouchEvent
方法,由于RecyclerView
没有重写dispatchTouchEvent
方法,所以走的依然是ViewGroup
的dispatchTouchEvent
方法。
如果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;
}
先看RecyclerView
的onInterceptTouchEvent
方法的返回值,如果mScrollState = SCROLL_STATE_DRAGGING
,返回值为true。上面代码中mScrollState = SCROLL_STATE_SETTLING
的情况下,才会设置mScrollState = SCROLL_STATE_DRAGGING
。我们知道对于RecyclerView
它丝毫没动的情况下,mScrollState=SCROLL_STATE_IDLE
的。所以对于ACTION_DOWN事件,RecyclerView
的onInterceptTouchEvent
方法的返回值为false。
什么情况下mScrollState = SCROLL_STATE_SETTLING
呢?这个状态是代码中RecyclerView
进行滑动,比如Fling操作的时候,RecyclerView的mScrollState = SCROLL_STATE_SETTLING
,此时还没有结束Fling滑动的话,此时手指按下,RecyclerView
的onInterceptTouchEvent
方法的返回值为true了。
回头继续分析RecyclerView
的onInterceptTouchEvent
方法返回值在ACTION_DOWN事件时返回false。此时就会到了RecyclerView
的dispatchTouchEvent
方法了,遍历RecyclerView的各个子View的情况我们先不考虑,就会走RecyclerView
的onTouchEvent
方法。
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;
}
从RecyclerView
的onTouchEvent
方法可以看出,返回值为true,说明只要点击区域在RecyclerView
区域内部,默认情况下RecyclerView
是拦截事件的。
至此,我们总结一下RecyclerView
对于ACTION_DOWN的事件处理。
首先CoordinatorLayout
的dispatchTouchEvent
方法接收到ACTION_DOWN事件,走它的onInterceptTouchEvent
方法,在这个方法中,分别调用AppBarLayout.Behavior
和AppBarLayout.ScrollingViewBehavior
的onInterceptTouchEvent
方法,都返回了false。因为对于AppBarLayout.Behavior
来说,点击区域在RecyclerView
上,所以它返回了false。对于AppBarLayout.ScrollingViewBehavior
来说,没有重写父类的onInterceptTouchEvent
方法,默认返回false。
此时事件就回到了CoordinatorLayout
的dispatchTouchEvent
方法,遍历各个子View,因为ACTION_DOWN事件在RecyclerView
的区域内,就会调用RecyclerView的dispatchTouchEvent
方法,先走RecyclerView
的onInterceptTouchEvent
方法,通常情况下该方法返回false。对于RecyclerView
的dispatchTouchEvent
方法经历遍历各个子View,没有找到处理事件的子View,就会走自己的onTouchEvent
方法,默认情况下RecyclerView
的onTouchEvent
方法返回true。紧接着返回值向上追溯,就会到CoordinatorLayout
的dispatchTouchEvent
方法遍历子View找到了处理事件的子View。那么后续的ACTION_MOVE、ACTION_UP事件,就会交给RecyclerView
进行处理。
可以看出RecyclerView
的事件拦截处理非常常规,跟Behavior
关系不大。
当RecyclerView
拦截了ACTION_DOWN事件后,后续的ACTION_MOVE和ACTION_UP自然就交给RecyclerView
的onTouchEvent
方法来处理,自然就可以滑动起来。
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
方法,上文已经说明RecyclerView
的Behavior
时AppBarLayout.ScrollgingViewBehavior
,在AppBarLayout.ScrollgingViewBehavior
类中有如下源码:
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}
这行代码就是告诉CoordiantorLayout
组件,RecyclerView
的Behavior
是依赖于AppBarLayout
组件的。如果AppBarLayout
组件布局变化了,告诉RecyclerView
,然后RecyclerView
就知道了。然后AppBarLayout.ScrollgingViewBehavior
的onDependentViewChanged
方法就接着被调用。
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
直接进行嵌套滑动。此时就会判断dx
和dy
的值
5 如果dx
=0且dy
=0,说明没有滑动距离.只有当dx
和dy
只要有一个不等于0即可。
具体dx
=0还是dy
=0要看mView
的设置。
以RecyclerView
来举例。
如果RecyclerView
设置的垂直滑动,不能水平滑动,该方法的dx
必定=0的。
如果RecyclerView
设置的水平滑动,不能垂直滑动,该方法的dy
必定=0的。
如果RecyclerView
同时支持水平滑动和垂直滑动,该方法的dx
和dy
都可能不等于0。
offsetInWindow
这个参数是个两个元素的数组,具体是保存mView
在整个window界面的位置的。初始值为0。
consumed
这个值也是两个元素的数组,初始值为0。这个值的目的是传递给p
之后,设置p
消耗的距离。
dx
和dy
代表触发的滑动距离,也就是p
这一次能够最大滑动的距离,consumed
代表实际消耗的距离,如果p
消耗了全部可滑动的距离,那么consumed
的值与dx
和 dy
的值是相等的。
所以这两行代码
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
处理,调用p
的onNestedScroll
方法,同时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
之后,直接调用p
的onStopNestedScroll
方法。同时将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
组件。
直接看CooridnatorLayout
的onStartNestedScroll
方法:
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;
}
从CooridnatorLayout
的onStartNestedScroll
方法可以看出,它简单的遍历了子View,如果子View的Behavior
的onStartNestedScroll
方法返回true,自己的onStartNestedScroll
方法就返回true。
紧接着就走到了AppBarLayout
的Behavior
,也就是AppBarLayout.Behavoir
的onStartNestedScroll
方法:
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.Behavoir
的onStartNestedScroll
方法主要是计算AppBarLayout
是否能够上下滑动作为返回值的。
如果不是上下滑动,AppBarLayout.Behavoir
的onStartNestedScroll
方法就返回false了。
如果 AppBarLayout
没有滑出屏幕外面,并且是上下滑动,那么started=true
。
往回追溯,CooridnatorLayout
的onStartNestedScroll
方法返回true,再追溯到NestedScrollingChildHelper
对象的startNestedScroll
方法找到了p
。
这样ACTION_DOWN事件的嵌套处理逻辑已经完成,紧接着RecyclerView
的onTouchEvent
方法处理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]
的值。
下面看CoordiantorLayout
的onNestedPreScroll
方法源码:
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
,分别调用Behavior
的onNestedPreScroll
方法,并把自己的消耗距离设置到consumed[0]
和consumed[1]
,最后accepted=true
的情况下,调用onChildViewsChanged
方法。
在CoordiantorLayout
的onNestedPreScroll
方法内部,会调用各个子View的Behavior
的
onNestedPreScroll
方法。
也就是AppBarLayout
的Behavior
中的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));
}
}
AppBarLayout
的Behavior
中的onNestedPreScroll
方法在处理向下滑动和向上滑动的逻辑是不一样的。
min != max
的情况下,才会在onNestedPreScroll
方法中消耗距离。
当手指向上滑动的时候,dy
>0,此时min
和max
值是不等的,scroll
方法才会调用。
当手指向下滑动的时候,dy
<0,此时min
和max
值相等,scroll
方法不会调用。
向上滑动的时候,日志如下:
向下滑动的时候,日志如下:
AppBarLayout
的Behavior
中的onNestedPreScroll
方法在处理向下滑动和向上滑动的逻辑是
当手指向上滑动的时候,dy
>0,此时min
和max
值是不等的,scroll
方法才会调用。此时AppBarLayout
的布局已经改变,接着在CoordiantorLayout
的onNestedPreScroll
方法中的onChildViewsChanged
方法被调用,接着ScrollingViewBehavior
的onDependViewChanged
方法就会被调用,然后RecyclerVIew
就跟着嵌套滑动了。
当手指向下滑动的时候,dy
<0,此时min
和max
值相等,scroll
方法不会调用。AppBarLayout
的布局没有改变,接着在CoordiantorLayout
的onNestedPreScroll
方法中的onChildViewsChanged
方法被调用,但是AppBarLayout
的布局没有改变,所以ScrollingViewBehavior
的onDependViewChanged
方法就不会被调用。
向上追溯代码,回到NestedScrollingChildHelper
中的dispatchNestedPreScroll
方法,它的dispatchNestedPreScroll
方法的返回值如果有消耗距离consumed[0] != 0 || consumed[1] != 0
,返回值就为true。
在向上追溯代码,回到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();
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);
}
继续向下走NestedScrollingChildHelper
的dispatchNestedScroll
方法,该方法就会走CoordinatorLayout
的onNestedScroll
方法,其源码如下:
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的Behavior
的onNestedScroll
方法,最后调用onChildViewsChanged
方法。如果AppBarLayout
的布局变化了,就通过遍历子View的onDependViewChanged
方法通知RecyclerView
进行嵌套滑动。
下面看AppBarLayout.Behavior
的onNestedScroll
方法源码:
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
方法中进行了处理。然后回到CoordinatorLayout
的onNestedScroll
方法中,调用onChildViewsChanged
通知RecyclerView
进行嵌套滑动。
5.11.3 小结
六 讨论
6.1
Behavior在嵌套滑动中起着什么样的作用?CoordinatorLayout组件又有什么作用?
Behavior
在嵌套滑动中作用相当于粘合剂的作用。各个View实现各个的Behavior
,具体Behavior
实现自己的逻辑,但是Behavior
的逻辑的相互之间的逻辑实现,是通过CoordinatorLayout
作为中间层实现的,起到中间转发的作用。例如Demo中的AppBarLayout
中的滑动,RecyclerView
要嵌套滑动就是通过CoordinatorLayout
中的监听器OnPreDrawListener
的方法中调用RecyclerView
的Behavior
的onDependedViewChanged
方法实现的。再看RecyclerView
滑动的是时候,AppBarLayout
的Behavior
中的方法onStartNestedScroll
、onNestedScrollAccepted
、onNestedPreScroll
、onNestedScroll
等等方法,这样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()
}
}
}
具体解决方法不止这一个,肯定有其他更好的方法。