我们都知道,如果想要使用CoordinatorLayout
实现折叠布局,只有靠AppBarLayout
才会生效。但是我们不禁有一个疑问,就是为什么AppBarLayout
能够与RecyclerView
联动,它是怎么知道RecyclerView
上滑还是下滑的呢?这是本文分析的一个重点。
本文参考资料:
由于联动机制是建立在嵌套滑动的基础上,所以在阅读本文之前,建议熟悉一下Android中嵌套滑动的原理,有兴趣的同学也可以参考我上面的文章。
本文打算采用由浅入深的方式来介绍联动机制,分别包括如下内容:
CoordinatorLayout
的分析Behavior
的分析
1. CoordinatorLayout的分析
在这里,我们先分析一下CoordinatorLayout
整体结构,包括三大流程,以及Behavior
的相关调用。我们都知道,在CoordinatorLayout
中,Behavior
是作为一个插件角色存在的,所以我们有必要分析一下,CoordinatorLayout
是怎么使用这个插件。熟悉插件的整个流程之后,后续我们在自定义Behavior
时就非常容易了。
(1). CoordinatorLayout的三大流程
CoordinatorLayout
的measure过程相较于其他View来说,还是稍微有一点特殊性。CoordinatorLayout
作为协调者布局,自然需要处理各个View的依赖关系,所有View的依赖关系形成了图的数据结构,因此每个View测量和布局都可能会受到其他View的影响,所以先测量哪些View,后测量哪些View,这里面需要有特殊的要求,不能通过简单的线性规则来进行。
因此,CoordinatorLayout
的measure过程先要对图进行拓补排序,得到一个线性的数列,然后才能进行下面的操作。我们先来看看CoordinatorLayout
的onMeasure
方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 得到一个图mChildDag,其中存储的是View之间的依赖关系;
// 同时,还得到一个拓补排序的数组。
prepareChildren();
ensurePreDrawListener();
// 测量每个View
}
整个过程我们可以将他分析两步:
- 构造依赖关系图,通过拓补排序得到一个数组。
- 根据拓补排序得到的数组顺序,来测量每个View。
在这个过程中,我们可以发现了Behavior
的影子,我们来看看代码:
final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
从上面的代码中,我们可以发现,View会尝试将测量工作交付给它的Behavior
,如果Behavior
不测量,然后再调用onMeasureChild
方法进行测量,这样做什么好处呢?有一个很大的特点就是Behavior
的高扩展性,在一些特殊的交互下,这些都是必须的。
这里我举一个例子,如图:
上图的布局非常的简单,这里就直接贴代码:
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator"
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"
android:fitsSystemWindows="true">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#5FF"
android:minHeight="50dp"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"/>
<View
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#500"
android:minHeight="50dp"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
效果非常的明显,就是AppBarLayout
第一个View会折叠,但是第二个View不会折叠,那么这个就影响到RecyclerView
的测量了,正常来说RecyclerView
的高度应该等于CoordinatorLayout
高度减去第二个View的高度,因为第二个View始终在屏幕当中。同理,如果AppBarLayout
只有一个View,同时这个View还能折叠,那么RecyclerView
的高度又不一样了。像这种不固定的测量规则,交给每个View的Behavior
是最好的。
同理,布局阶段也是如此,首先会交给Behavior
尝试着布局,然后CoordinatorLayout
再布局,这里就不详细介绍了。
(2).事件的协调
CoordinatorLayout
被定义为协调者布局,自然要起到协调的作用,那么它在哪里就行协调的呢?最大的体现就是,将子View传递上来的嵌套滑动事件进行分发。我总结一下相关方法:
- 嵌套事件开始,会回调
onStartNestedScroll
方法。- 嵌套滑动开始,会回调
onNestedPreScroll
方法。- 嵌套滑动结束,会回调
onNestedScroll
方法。- 嵌套滑动的Fling开始,会回调
onNestedPreFling
方法。- 嵌套滑动的Fling结束,会回调
onNestedFling
方法。
而CoordinatorLayout
方法是怎么进行协调的呢?在每个方法的实现里面,都通过每个View的Behavior
来分发,每个Behavior
在根据实际情况判断是否消费,消费多少。
我们在自定义Behavior
时,还有一个问题存在。就是如果我们使用的自定义View,然后通过一个特殊的方法来滑动该View,在CoordinatorLayout
里面将该View作为依赖的View都能随之移动,这种交互是怎么实现的呢?在这种情况下,我们根本不是嵌套滑动来响应的,而是通过一个OnPreDrawListener
接口来实现的,这个接口在View执行onDraw
方法之前被回调。同理,在这种情况下,只能实现联动,不能实现更多复杂的UI交互。
2.Behavior的分析
分析Behavior
时,我们先来看看它的基本结构,看看它有哪些方法,并且调用时机是什么。
方法名 | 作用或者调用时机 |
---|---|
layoutDependsOn | 判断两个是否存在依赖关系。 |
onDependentViewChanged | 当一个View发生变化(包括位置变化等变化)时,依赖其的View的Behavior 都会回调这个方法。 |
onDependentViewRemoved | 当一个View被移除时,依赖其的View的Behavior 都会回调这个方法。 |
Behavior
比较常用的方法就是如上的,其实还有嵌套滑动一些列的方法,这里就过多的解释。
单纯的看基类自然不能深入理解这个类使用方式,我们来看看它的实现类,主要是从两个方面来分析:
AppBarLayout
的几个Behavior
RecyclerView
常用的ScrollingViewBehavior
(1). AppBarLayout的Behavior分析
AppBarLayout
的Behavior
是一个复杂的继承关系,我们先来看看相关类图:
整个继承关系如上类图,每个类都负责其中一部分的功能,我们来看看:
类名 | 作用 |
---|---|
ViewOffsetBehavior | 在ViewOffsetBehavior 的内部,定义了两个方法,分别是setTopAndBottomOffset 和setLeftAndRightOffset ,主要用来改变某个View的位置。 |
HeaderBehavior | 在HeaderBehavior 中,主要是实现了两个事件分发相关的方法。在这个类里面,主要处理AppBarLayout 本身的事件,比如说,手指在AppBarLayout 上面滑动。在这个类里面,有一个非常恶心的设计,就是如果在AppBarLayout 上面Fling的话,会将所有的Fling吃掉,不会传递到RecyclerView 上面去。我个人感觉,Google爸爸的这个设计有问题,待会详细解释一下。 |
BaseBeHavior | 在BaseBehavior 中,主要是实现了嵌套滑动的相关方法。 |
AppBarLayout
的Behavior
整个结构差不多介绍清楚了,下面我来解释一下,为什么我觉得HeaderBehavior
的设计有问题。
首先,我觉得不应该多出来
HeaderBehavior
这一层。HeaderBehavior
主要作用是用来处理AppBarLayout
的事件(传统事件),将事件处理放在HeaderBehavior
里面有一个很大的缺陷,就是从此以后,AppBarLayout
的子View不支持嵌套滑动,因为在AppBarLayout
这一层就断了;其次,就是有一个很大的问题,Fling事件在HeaderBehavior
里面全部消耗了,本来可以将未消耗的Fling事件传递给RecyclerView的,但是这样的设计却很难将未消耗的Fling传递出去。
我的建议是将这部分事件方法在AppBarLayout
内部实现,其中既能保证嵌套滑动不断层,又能保证将未消耗的Fling事件传递到它的Parent中去。
在这里,我重点分析HeaderBehavior
和BaseBeHavior
。
(A). HeaderBehavior
HeaderBehavior
主要是对AppBarLayout
的事件进行处理,这里我们主要看fling事件,看看这里为什么不能将fling事件传递给RecyclerView
。
case MotionEvent.ACTION_UP:
if (velocityTracker != null) {
velocityTracker.addMovement(ev);
velocityTracker.computeCurrentVelocity(1000);
float yvel = velocityTracker.getYVelocity(activePointerId);
fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
}
核心关键点就在fling方法的第二个参数和第三个参数,分别表示fling
的最小距离和最大距离。因为最大距离是0,所以一旦AppBarLayout
滑出屏幕,fling就停止了。
针对这个问题,有很多解决办法,本文先不做描述,后续我会专门的文章来解决这个问题。
(B). BaseBeHavior
BaseBeHavior
的作用是主要两个:
- 处理
AppBarLayout
的嵌套滑动。- 负责
AppBarLayout
的测量和布局。
这里专门分析嵌套滑动,不对测量和布局做分析,因为比较简单。在分析之前,我们先来看AppBarLayout
几个方法:
方法 | 作用或者调用时机 |
---|---|
getDownNestedPreScrollRange | 计算AppBarLayout 能在RecyclerView 向下滑动之前,能提前向下滑动的距离。非常直观的感受是,一个View设置了SCROLL_FLAG_ENTER_ALWAYS 时,当RecyclerView 向下滑动时,该View首先向下滑动。该方法返回的值表示该View能向下滑动多少。 |
getUpNestedPreScrollRange | 作用于getDownNestedPreScrollRange 方法差不多,就是它表示向上能滑动的距离。 |
getDownNestedScrollRange | 计算当RecyclerView 滑动到顶部之后,AppBarLayout 能向下滑动的距离。非常直观的感受是,一个View设置了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED 时,当RecyclerView 滑动到顶部之后继续滑动时,此时该View会向下滑动。该方法返回的值表示该View能向下滑动多少。 |
getTotalScrollRange | 该方法表示AppBarLayout 能滑动的总距离,不区分方向。 |
BaseBeHavior
主要实现了嵌套滑动的onStartNestedScroll
、onNestedPreScroll
、onNestedScroll``onStopNestedScroll
这几个方法。接下来,我们来一一分析。
首先,我们来看看onStartNestedScroll
方法:
@Override
public boolean onStartNestedScroll(
CoordinatorLayout parent,
T child,
View directTargetChild,
View target,
int nestedScrollAxes,
int type) {
// Return true if we're nested scrolling vertically, and we either have lift on scroll enabled
// or we can scroll the children.
final boolean started =
(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& (child.isLiftOnScroll() || canScrollChildren(parent, child, directTargetChild));
if (started && offsetAnimator != null) {
// Cancel any offset animation
offsetAnimator.cancel();
}
// A new nested scroll has started so clear out the previous ref
lastNestedScrollingChildRef = null;
// Track the last started type so we know if a fling is about to happen once scrolling ends
lastStartedType = type;
return started;
}
这个方法表示意思非常的简单,就是判断AppBarLayout
是否需要处理嵌套滑动,其中判断条件分别是,滑动方向是垂直滑动,其次此时还有空间可以滑动。
然后,我们再来看看onNestedPreScroll
方法:
@Override
public void onNestedPreScroll(
CoordinatorLayout coordinatorLayout,
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));
}
}
onNestedPreScroll
方法要分为两种情况:1. RecyclerView
向下滑动;2.RecyclerVIew
向上滑动。这两种情况根据不同的Flag,计算能够滑动的距离。
再次,就是onNestedScroll
方法:
@Override
public void onNestedScroll(
CoordinatorLayout coordinatorLayout,
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);
}
}
这个方法的调用,只需要考虑到一种情况---RecyclerView向上滑动滑动,并且滑到了顶部,此时设置了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
Flag的View该滑动了。
最后就是onStopNestedScroll
方法:
@Override
public void onStopNestedScroll(
CoordinatorLayout coordinatorLayout, T abl, View target, int type) {
// onStartNestedScroll for a fling will happen before onStopNestedScroll for the scroll. This
// isn't necessarily guaranteed yet, but it should be in the future. We use this to our
// advantage to check if a fling (ViewCompat.TYPE_NON_TOUCH) will start after the touch scroll
// (ViewCompat.TYPE_TOUCH) ends
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
// If we haven't been flung, or a fling is ending
snapToChildIfNeeded(coordinatorLayout, abl);
if (abl.isLiftOnScroll()) {
abl.setLiftedState(abl.shouldLift(target));
}
}
// Keep a reference to the previous nested scrolling child
lastNestedScrollingChildRef = new WeakReference<>(target);
}
onStopNestedScroll
方法主要是对设置FLAG_SNAP
的View做动画。
到这里,我们发现一个问题,那就是BaseBeHavior
没有重写Fling相关方法,但是实际情况是AppBarLayout
能成功响应RecyclerView
的Fling事件,这个是怎么实现的呢?
最初,我以为是BaseBehavior
会监听RecyclerView
的位置变化,通过onDependentViewChanged
方法来响应Fling事件,结果发现BaseBehavior
根本没有实现这个方法,那BaseBehavior
方法是怎么实现的呢?
这个问题需要从RecyclerView
的ViewFlinger
找答案。对于不熟悉RecyclerView
的同学来说,我来解释一下,ViewFlinger
到底是什么。ViewFlinger
主要是用来出来RecyclerView
的Fling事件的。如果有同学对他感兴趣的话,可以参考我的文章:RecyclerView 源码分析(二) - RecyclerView的滑动机制。在ViewFlinger
中有如下一段代码:
if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null,
TYPE_NON_TOUCH)) {
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
}
从这段代码里面,我们可以发现,RecyclerView
在Fling期间也会调用dispatchNestedPreScroll
方法,从而调用到BaseBeHavior
的onNestedPreScroll
方法,所以onNestedPreScroll
方法会处理两部分的滑动距离,包括正常滑动和Fling滑动。
(2).RecyclerView的Behavior分析
RecyclerView
的Behavior
继承结构与AppBarLayout
的类似,我们来看看类图:
这其中,
HeaderScrollingViewBehavior
和ScrollingViewBehavior
方法含义如下:
类名 | 作用 |
---|---|
HeaderScrollingViewBehavior | 重写了onMeasureChild 方法和onLayoutChild 方法,主要负责RecyclerView 的测量和布局。 |
ScrollingViewBehavior | 重写了layoutDependsOn 方法和onDependentViewChanged 方法。主要是负责RecyclerView 与AppBarLayout 联动。 |
接下来,我们一一的来分析。
(A).HeaderScrollingViewBehavior
在这里,我们重点关注HeaderScrollingViewBehavior
测量时如何考虑到AppBarLayout
的有效高度,具体代码如下:
int height = availableHeight + getScrollRange(header);
int headerHeight = header.getMeasuredHeight();
if (shouldHeaderOverlapScrollingChild()) {
child.setTranslationY(-headerHeight);
} else {
height -= headerHeight;
}
我们发现,在计算RecyclerView
的高度时,还加上了AppBarLayout
的可以滑动的距离。也就是说,当我们首次进入界面时,表面上看RecyclerView
布满屏幕,其实还有一部分在屏幕呢。
同样的,布局也是考虑到AppBarLayout
的,这里就不分析了。
(B). ScrollingViewBehavior
ScrollingViewBehavior
主要负责RecyclerView
与AppBarLayout
的联动,关键代码在于onDependentViewChanged
方法:
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
offsetChildAsNeeded(child, dependency);
updateLiftedStateIfNeeded(child, dependency);
return false;
}
具体的实现这里就不分析了,非常的简单。
3. 总结
到这里,本文的介绍结束了,这里做本文的内容做一个简单的总结。
CoordinatorLayout
在测量阶段,会生成一个View的依赖图,然后对这个依赖图进行拓补排序得到一个数组,测量和layout的顺序都依据一个数组的。CoordinatorLayout
测量和布局View
的工作首先会交给每个View的Behavior
,如果不处理才自己处理。AppBarLayout
的Behavior
分为三层,分别是:ViewOffsetBehavior
,方便改变View的位置;HeaderBehavior
用来处理AppBarLayout
自身的事件;BaseBeHavior
用来处理嵌套滑动的事件。RecyclerView
的Behavior
也分为三层:第一层与AppBarLayout
的一样;HeaderScrollingViewBehavior
负责RecyclerView
的测量和布局;ScrollingViewBehavior
处理RecyclerView
与AppBarLayout
的联动。
如果不出意外的话,下篇文章我将介绍怎么自定义Behavior
和处理AppBarLayout
的fling事件。