我们都知道,如果想要使用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的几个BehaviorRecyclerView常用的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_COLLAPSEDFlag的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事件。








网友评论