咱们都知道,若是想要使用CoordinatorLayout
实现折叠布局,只有靠AppBarLayout
才会生效。可是咱们不由有一个疑问,就是为何AppBarLayout
可以与RecyclerView
联动,它是怎么知道RecyclerView
上滑仍是下滑的呢?这是本文分析的一个重点。 本文参考资料:android
因为联动机制是创建在嵌套滑动的基础上,因此在阅读本文以前,建议熟悉一下Android中嵌套滑动的原理,有兴趣的同窗也能够参考我上面的文章。 本文打算采用由浅入深的方式来介绍联动机制,分别包括以下内容:数组
CoordinatorLayout
的分析Behavior
的分析
在这里,咱们先分析一下CoordinatorLayout
总体结构,包括三大流程,以及Behavior
的相关调用。咱们都知道,在CoordinatorLayout
中,Behavior
是做为一个插件角色存在的,因此咱们有必要分析一下,CoordinatorLayout
是怎么使用这个插件。熟悉插件的整个流程以后,后续咱们在自定义Behavior
时就很是容易了。bash
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
}
复制代码
整个过程咱们能够将他分析两步:app
- 构造依赖关系图,经过拓补排序获得一个数组。
- 根据拓补排序获得的数组顺序,来测量每一个View。
在这个过程当中,咱们能够发现了Behavior
的影子,咱们来看看代码:ide
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
再布局,这里就不详细介绍了。布局
CoordinatorLayout
被定义为协调者布局,天然要起到协调的做用,那么它在哪里就行协调的呢?最大的体现就是,将子View传递上来的嵌套滑动事件进行分发。我总结一下相关方法:动画
- 嵌套事件开始,会回调
onStartNestedScroll
方法。- 嵌套滑动开始,会回调
onNestedPreScroll
方法。- 嵌套滑动结束,会回调
onNestedScroll
方法。- 嵌套滑动的Fling开始,会回调
onNestedPreFling
方法。- 嵌套滑动的Fling结束,会回调
onNestedFling
方法。
而CoordinatorLayout
方法是怎么进行协调的呢?在每一个方法的实现里面,都经过每一个View的Behavior
来分发,每一个Behavior
在根据实际状况判断是否消费,消费多少。 咱们在自定义Behavior
时,还有一个问题存在。就是若是咱们使用的自定义View,而后经过一个特殊的方法来滑动该View,在CoordinatorLayout
里面将该View做为依赖的View都能随之移动,这种交互是怎么实现的呢?在这种状况下,咱们根本不是嵌套滑动来响应的,而是经过一个OnPreDrawListener
接口来实现的,这个接口在View执行onDraw
方法以前被回调。同理,在这种状况下,只能实现联动,不能实现更多复杂的UI交互。ui
分析Behavior
时,咱们先来看看它的基本结构,看看它有哪些方法,而且调用时机是什么。
方法名 | 做用或者调用时机 |
---|---|
layoutDependsOn | 判断两个是否存在依赖关系。 |
onDependentViewChanged | 当一个View发生变化(包括位置变化等变化)时,依赖其的View的Behavior 都会回调这个方法。 |
onDependentViewRemoved | 当一个View被移除时,依赖其的View的Behavior 都会回调这个方法。 |
Behavior
比较经常使用的方法就是如上的,其实还有嵌套滑动一些列的方法,这里就过多的解释。 单纯的看基类天然不能深刻理解这个类使用方式,咱们来看看它的实现类,主要是从两个方面来分析:
AppBarLayout
的几个Behavior
RecyclerView
经常使用的ScrollingViewBehavior
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
。
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就中止了。 针对这个问题,有不少解决办法,本文先不作描述,后续我会专门的文章来解决这个问题。
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滑动。
RecyclerView
的Behavior
继承结构与AppBarLayout
的相似,咱们来看看类图:
HeaderScrollingViewBehavior
和
ScrollingViewBehavior
方法含义以下:
类名 | 做用 |
---|---|
HeaderScrollingViewBehavior | 重写了onMeasureChild 方法和onLayoutChild 方法,主要负责RecyclerView 的测量和布局。 |
ScrollingViewBehavior | 重写了layoutDependsOn 方法和onDependentViewChanged 方法。主要是负责RecyclerView 与AppBarLayout 联动。 |
接下来,咱们一一的来分析。
在这里,咱们重点关注HeaderScrollingViewBehavior
测量时如何考虑到AppBarLayout
的有效高度,具体代码以下:
int height = availableHeight + getScrollRange(header);
int headerHeight = header.getMeasuredHeight();
if (shouldHeaderOverlapScrollingChild()) {
child.setTranslationY(-headerHeight);
} else {
height -= headerHeight;
}
复制代码
咱们发现,在计算RecyclerView
的高度时,还加上了AppBarLayout
的能够滑动的距离。也就是说,当咱们首次进入界面时,表面上看RecyclerView
布满屏幕,其实还有一部分在屏幕呢。 一样的,布局也是考虑到AppBarLayout
的,这里就不分析了。
ScrollingViewBehavior
主要负责RecyclerView
与AppBarLayout
的联动,关键代码在于onDependentViewChanged
方法:
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
offsetChildAsNeeded(child, dependency);
updateLiftedStateIfNeeded(child, dependency);
return false;
}
复制代码
具体的实现这里就不分析了,很是的简单。
到这里,本文的介绍结束了,这里作本文的内容作一个简单的总结。
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事件。