本节我们来介绍 CoordinatorLayout 的工作原理,如果对 CoordinatorLayout 的基本使用还不熟悉,可以先参考下基本使用篇 CoordinatorLayout(一)—— 基本使用
一、CoordinatorLayout 功能分析
CoordinatorLayout 可以看成是对嵌套滑动机制的应用或扩展,它有如下四大功能:
- 处理子控件之间依赖下的交互:CoordinatorLayout 的子控件可以依赖某一个子控件,根据该依赖的变化做出相应的改变
- 处理子控件之间的嵌套滑动:嵌套滑动机制本身只能是 NestedScrollingChild 与 NestedScrollingParent 以一对一的形式进行交互,但是 CoordinatorLayout 将其发扬光大为一对多。比如滑动一个 RecyclerView 时,其上方 AppBarLayout 与下方 BottomNavigationView 都可以做出相应的行为变化
- 处理子控件的测量与布局
- 处理子控件的事件拦截与响应
以上功能都是建立在 CoordinatorLayout 提供的一个 Behavior —— 定义子 View 之间交互行为的插件之上,其内部提供了相应方法来应对这四个不同的功能:
下面我们来看看 CoordinatorLayout 是如何借助 Behavior 实现上面 4 个功能的。
1.子控件之间的依赖交互
当 CoordinatorLayout 中的子 View 需要依赖于其他子 View 时,会涉及到两个问题:
- 依赖于谁?
- 被依赖的 View 发生变化(大小、位置)时,该子 View 如何应变?
Behavior 提供了如下三个方法来解决以上两个问题:
方法名 | 参数 | 方法介绍 |
---|---|---|
layoutDependsOn | CoordinatorLayout parent:第二个参数 child 的父容器。 V child:进行测试的子 View。 View dependency:可能被 child 依赖的 View。 | 确定所提供的子视图是否有另一个特定的同级视图作为布局依赖项。在响应布局请求时,将至少调用此方法一次。 如果该方法对于给定的 <child,dependency> 对返回 true,那么作为父容器的 parent 将总是先摆放 dependency 再摆放 child。 |
onDependentViewChanged | CoordinatorLayout parent:第二个参数 child 的父容器。 V child:待处理的子 View。 View dependency:被依赖并且发生了变化的 View。 | 当被依赖的 dependency 发生变化(大小、位置)时,Behavior 会回调此方法以更新 child(正常的 layout 流程不会回调)。 一个 View 的依赖是由 layoutDependsOn() 决定的,或者 child 已经设置了另一个 View 作为它的锚点。 如果一个 Behavior 通过这个方法改变了 child 的布局,也需要在 onLayoutChild() 中重建正确的位置。 方法默认返回 false,如果 Behavior 改变了 child 的大小或位置,则返回 true。 |
onDependentViewRemoved | CoordinatorLayout parent:第二个参数 child 的父容器。 V child:待处理的子 View。 View dependency:被依赖并且被移除的 View。 | dependency 从它的父容器中被移除后,Behavior 会回调此方法以更新 child。 |
第一个方法可以帮助我们建立 CoordinatorLayout 内各个子 View 之间的依赖关系,下面看看依赖关系是如何建立的。
依赖关系的建立
假如让你想办法获取 CoordinatorLayout 内各个子 View 之间的依赖关系,你会怎样实现?我想首先要考虑好如下两个问题:
- 在什么时候去获取依赖关系
- 如何恰当的表示依赖关系
对于问题 1,我们不妨从 View 的生命周期的角度考虑。创建、测量、布局、绘制,哪一个最合适?
- 首先我们要明确获取依赖关系一定是在布局之前,因为一个很浅显的道理就是,我在布局子 View 时,一定是先去给被依赖最多的那个子 View 布局吧?比如 A 依赖 B,B 依赖 C,那我一定是先让 C 作为锚点去进行布局,然后才是 B 最后才是 A。因此依赖关系要在布局之前获取
- 其次我们再考虑是在创建还是测量时获取依赖关系。假如在创建 CoordinatorLayout 时获取,由于 View 在创建完毕后才会回调生命周期中的 onFinishInflate(),然后才经由 Activity 的 onStart()、onResume() 再到 View 的 onAttachedToWindow(),在还没有被添加到 Window 时就去获取依赖关系有些早,并且对于创建过程的效率也有影响。反观如果放在测量中,刚好可以在需要依赖关系的布局之前准备好依赖关系数据,是最合适获取依赖关系的时机
对于问题 2,源码中采用的是有向无环图 DirectedAcyclicGraph 来表示各个子 View 之间的依赖关系,下面我们直接结合源码来看。
在 CoordinatorLayout 的 onMeasure() 的第一行代码就执行了一个 prepareChildren():
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
...
}
该方法就是获取依赖关系并建立相应数据结构的方法:
// 有向无环图,存放 CoordinatorLayout 的子 View 及其依赖的 View
private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();
// 对 mChildDag 的 key 进行了拓扑排序后的子 View 集合,依赖少的排在前面
private final List<View> mDependencySortedChildren = new ArrayList<>();
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();
// 外层循环遍历所有子 View
for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);
// 找到 AnchorView
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);
// 将子 View 作为节点添加到 mChildDag 中
mChildDag.addNode(view);
// 内层循环,看 view 是否依赖于其他子 View,有的话就在两个子 View 间加一条边
for (int j = 0; j < count; j++) {
// 排除 view 自己
if (j == i) {
continue;
}
final View other = getChildAt(j);
// 检查 view 是否依赖于 other,如果是就在二者之间添加一条边表示有依赖关系
if (lp.dependsOn(this, view, other)) {
// 如果 mChildDag 中还没有添加 other 这个节点,就先添加它
if (!mChildDag.contains(other)) {
// Make sure that the other node is added
mChildDag.addNode(other);
}
// 在有向无环图中添加一条边,表示 view 依赖于 other,内部其实是用 SimpleArrayMap<T, ArrayList<T>>
// 表示有向无环图,让 other 作为 key,将 view 添加到对应的 ArrayList 中
mChildDag.addEdge(other, view);
}
}
}
// getSortedList() 得到拓扑排序的倒序集合,添加到 mDependencySortedChildren 后取倒序得到正向的拓扑排序集合,
// 正序集合的第一个元素是没有任何依赖的子 View
// Finally add the sorted graph list to our list
mDependencySortedChildren.addAll(mChildDag.getSortedList());
// We also need to reverse the result since we want the start of the list to contain
// Views which have no dependencies, then dependent views after that
Collections.reverse(mDependencySortedChildren);
}
通过双层循环建立依赖关系,外层循环遍历所有子 View,内层循环就找该子 View 依赖的 View,有依赖关系就添加一条边到 mChildDag 中。mChildDag 内部用一个 SimpleArrayMap<T, ArrayList<T>>
表示有向无环图,key 是各个子 View,value 是依赖于 key 的子 View 集合。
对 mChildDag 的 key 进行了拓扑排序后的结果保存在 mDependencySortedChildren 中并取了一个倒序,原因是 DirectedAcyclicGraph 的 getSortedList() 返回的是基于 DFS 实现的拓扑排序的反序列表:
@NonNull
public ArrayList<T> getSortedList() {
// 排序结果集合
mSortResult.clear();
// 排序用到的临时标记集合
mSortTmpMarked.clear();
// 深度遍历有向无环图中的每一个节点
for (int i = 0, size = mGraph.size(); i < size; i++) {
dfs(mGraph.keyAt(i), mSortResult, mSortTmpMarked);
}
return mSortResult;
}
private void dfs(final T node, final ArrayList<T> result, final HashSet<T> tmpMarked) {
if (result.contains(node)) {
// We've already seen and added the node to the result list, skip...
return;
}
// 如果临时标记用的集合中之前已经有了 node 这个节点,说明依赖关系形成闭环了,要抛异常
if (tmpMarked.contains(node)) {
throw new RuntimeException("This graph contains cyclic dependencies");
}
// Temporarily mark the node
tmpMarked.add(node);
// 深度遍历 node 上所有边连接到的点
final ArrayList<T> edges = mGraph.get(node);
if (edges != null) {
for (int i = 0, size = edges.size(); i < size; i++) {
dfs(edges.get(i), result, tmpMarked);
}
}
// Unmark the node from the temporary list
tmpMarked.remove(node);
// Finally add it to the result list
result.add(node);
}
由 dfs 方法能看出,先添到结果中的应该是深度遍历尾部,没有被其他节点依赖的那个节点,也就是拓扑排序的终点,最后才添加拓扑排序的起点,就是说结果与标准的拓扑排序算法是相反的,所以 mDependencySortedChildren 才会在 addAll() 之后再进行一次 reverse 操作得到正序的拓扑排序结果。
用拓扑排序的原因是,当子 View 发生变化时,需要优先处理没有依赖的子 View(拓扑排序起点),然后是已经处理完依赖的子 View(中间节点),最后处理依赖最多的子 View(终点)。比如,假设 C 依赖于 B,B 依赖于 A,现在 A 变了,那处理的顺序一定是 B 先根据 A 的变化而变化,然后 C 再跟着 B 变化,如果 C 先于 B 变化,那等 B 变完了之后,C 还要再次根据 B 的变化调整自己,白做了一次调整。
子 View 间交互的实现
前面说到 Behavior 提供了三个方法来支持子 View 之间的依赖交互,其中 onDependentViewChanged() 和 onDependentViewRemoved() 两个方法会在子 View 发生变化和被移除时回调给依赖该 View 的其他子 View。那么问题来了,如何知道子 View 发生了变化或者被移除了呢?
这里说的子 View 的变化具体是指大小和位置的变化,不包括 View 内部绘制内容的变化,比如嵌套滑动造成的 View 的内容的变化在下一个小节的讨论范围内。
其实是设置了两个监听器 ViewTreeObserver.OnPreDrawListener 和 HierarchyChangeListener。
ViewTreeObserver 顾名思义,是视图树监听者,可以在该监听者上注册以下常用接口以获取视图树相关信息:
接口名 | 作用 |
---|---|
OnWindowAttachListener | 当视图层次结构关联到窗口或与之分离时回调 |
OnWindowFocusChangeListener | 当视图层次结构的窗口焦点状态发生变化时回调 |
OnGlobalFocusChangeListener | 当视图树中的焦点状态更改时回调 |
OnGlobalLayoutListener | 当全局布局状态或视图树中视图的可见性更改时回调 |
OnPreDrawListener | 当视图即将绘制时回调 |
OnDrawListener | 当视图树绘制时回调 |
OnTouchModeChangeListener | 当触摸模式改变时回调 |
OnScrollChangedListener | 当视图树中的某些内容被滚动时回调 |
ViewTreeObserver 不能被实例化,其实例可以通过 View.getViewTreeObserver() 获取。CoordinatorLayout 在 onAttachedToWindow() 中添加了 OnPreDrawListener:
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors(false);
// mNeedsPreDrawListener 在 addPreDrawListener() 中被置为 true,在
// removePreDrawListener() 中被置为 false,只要 CoordinatorLayout 的子
// View 中有被依赖的子 View,系统就会调用方法使 mNeedsPreDrawListener 为 true
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
...
mIsAttachedToWindow = true;
}
mOnPreDrawListener 是 CoordinatorLayout 定义的 ViewTreeObserver.OnPreDrawListener 的实现类实例:
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
// 传入参数必须是 @DispatchChangeEvent 注解定义的三个值之一:
// EVENT_PRE_DRAW, EVENT_NESTED_SCROLL, EVENT_VIEW_REMOVED,
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
接口方法 onPreDraw() 会在 ViewTreeObserver 的 dispatchOnPreDraw() 中被调用:
/**
* 通知观察者绘制即将开始,如果其中的某个观察者返回 false,那么绘制将会取消,并且重新安排
* 绘制,如果想在 View Layout 或 View hierarchy 还未依附到 Window 时,或者在 View 处于
* GONE 状态时强制绘制,可以手动调用这个方法
* 方法如返回 true 表示绘制需要被取消并重新安排
*/
public final boolean dispatchOnPreDraw() {
boolean cancelDraw = false;
final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
if (listeners != null && listeners.size() > 0) {
CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
// 如果有一个 onPreDraw() 返回 false,那么 cancelDraw 就会为 true,
// cancelDraw 作为返回值,如果为 true 的话就会取消本次绘制
cancelDraw |= !(access.get(i).onPreDraw());
}
} finally {
listeners.end();
}
}
return cancelDraw;
}
再看 onChildViewsChanged() 的内容:
/**
* 向关联的 Behavior 实例分发依赖视图变化。
*
* 当至少一个子视图报告会依赖另一个视图时,通常作为预绘制(pre-draw)的一部分,
* 这允许 CoordinatorLayout 考虑发生在正常布局过程之外的布局更改和动画。
*
* 它也可以作为嵌套滚动分派的一部分运行,以确保任何偏移在正确的坐标窗口内完成。
* 此处实现的偏移行为不会在 LayoutParams 中存储计算得出的偏移量;相反,它期望
* 布局过程始终重构正确的定位。
*/
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
// 按照拓扑排序的顺序从集合中取出节点进行处理
final int childCount = mDependencySortedChildren.size();
// 从 Pools.Pool<Rect> 中获取 Rect 对象
final Rect inset = acquireTempRect();
final Rect drawRect = acquireTempRect();
final Rect lastDrawRect = acquireTempRect();
// 外层循环,负责从 mDependencySortedChildren 中取出 child
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;
}
......
// 从 i+1,即当前子 View 的后续节点开始取 checkChild,判断 checkChild 是否依赖于 child
for (int j = i + 1; j < childCount; j++) {
// 内层循环,取出 checkChild,并获取它的 LayoutParams 和 Behavior
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
// 如果 Behavior 不为 null 并且 checkChild 依赖于 child 的话,就根据 type 类型进行分发
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
// getChangedAfterNestedScroll() 是去获取 mDidChangeAfterNestedScroll 标记位,
// 如果类型是 EVENT_PRE_DRAW 并且 View 是在嵌套滑动之后发生变化,就跳过分发过程并重置该 flag
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
// If this is from a pre-draw and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
// 如果是因为 View 被移除了而调用本方法的,就让 Behavior 回调 onDependentViewRemoved()
// 默认(EVENT_PRE_DRAW、EVENT_NESTED_SCROLL)让 Behavior 回调 onDependentViewChanged()
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:
// EVENT_VIEW_REMOVED 时确定是有 View 发生了变化,所以 handled 直接给了 true,
// 但在 default 时要让 handled 赋值为 onDependentViewChanged() 的返回值
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
// 记录是否在嵌套滑动之后有 View 发生了变化
if (type == EVENT_NESTED_SCROLL) {
// If this is from a nested scroll, set the flag so that we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
// 释放 Rect 资源
releaseTempRect(inset);
releaseTempRect(drawRect);
releaseTempRect(lastDrawRect);
}
以上是针对 ViewTreeObserver.OnPreDrawListener 的工作流程。另外一个接口 HierarchyChangeListener 是在构造方法中被设置,会在视图树发生变化(增加、移除 View)时回调:
public CoordinatorLayout(@NonNull Context context, @Nullable AttributeSet attrs,
@AttrRes int defStyleAttr) {
......
super.setOnHierarchyChangeListener(new HierarchyChangeListener());
}
private class HierarchyChangeListener implements OnHierarchyChangeListener {
HierarchyChangeListener() {
}
@Override
public void onChildViewAdded(View parent, View child) {
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
@Override
public void onChildViewRemoved(View parent, View child) {
onChildViewsChanged(EVENT_VIEW_REMOVED);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
在 onChildViewRemoved() 中也调用了 onChildViewsChanged(),只不过参数传递的是 EVENT_VIEW_REMOVED 表示 View 已经被移除。
总结一下 CoordinatorLayout 是如何实现子控件之间的依赖交互的:
- onMeasure() 时遍历所有子 View,将子 View 之间的依赖关系添加到一个有向无环图中,并对其进行拓扑排序
- 当有 View 发生了变化时,对第 1 步中拓扑排序集合进行遍历,找出依赖它们的子 View,根据不同的事件类型,通过 Behavior 回调相应的方法(onDependentViewChanged() 或 onDependentViewRemoved())
2.子控件之间的嵌套滑动
CoordinatorLayout 实现了 NestedScrollingParent2&3 接口,那么当发生嵌套滑动时,在 NestedScrollingChild 滑动之前,会先询问嵌套滑动的父容器 CoordinatorLayout 是否要进行滑动。CoordinatorLayout 并没有亲自处理嵌套滑动,而是转交给 Behavior,由 Behavior 实现嵌套滑动逻辑:
如果对嵌套滑动不了解的话可以先看看 Android嵌套滑动 。
CoordinatorLayout 实现 NestedScrollingParent2 的接口方法内部都有把嵌套滑动事件交给 Behavior 同名方法的处理的逻辑,比如说:
@Override
@SuppressWarnings("unchecked")
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);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
// 已经接收过嵌套滑动事件的子 View 也要跳过
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
// 拿到 Behavior
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mBehaviorConsumed[0] = 0;
mBehaviorConsumed[1] = 0;
// 交给 Behavior 的同名方法处理
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;
// 如果 Behavior 处理了,调用 onChildViewsChanged,类型为 EVENT_NESTED_SCROLL
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
从上述代码中可以看出:
- 能接收到嵌套滑动事件的只能是设置了 Behavior 的直接子 View,并且处于 GONE 状态或者已经接收过嵌套滑动事件的子 View 无法接收嵌套滑动事件
- 如果 Behavior 消费了嵌套滑动事件,那么会调用 onChildViewsChanged() 并传入 EVENT_NESTED_SCROLL。实际上除了上面的 onNestedPreScroll(),还有 onNestedScroll() 和 onNestedFling() 也传入了 EVENT_NESTED_SCROLL
需要注意的是,与响应/处理嵌套滑动事件不同,产生嵌套滑动事件的可以是 CoordinatorLayout 下任何层级的 NestedScrollingChild ,因为 NestedScrollingChild 在开始嵌套滑动事件时,会遍历其所有父容器以找到一个可以处理嵌套滑动的 NestedScrollingParent,这个 NestedScrollingParent 可以不是 NestedScrollingChild 的直接父容器。
这样一来,原本嵌套滑动中只有父子两个角色,在 CoordinatorLayout 中就又增加了一个 Behavior:
从而实现了嵌套滑动在兄弟控件之间的交互(ChildView1 产生了嵌套滑动事件,父容器 CoordinatorLayout 自己没有处理,而是交给了其他子 View ChildView2 甚至 ChildView3 处理滑动,实现兄弟间的交互,也将嵌套滑动的关系从一对一进化为一对多)。
3.子控件的测量与布局
CoordinatorLayout 主要负责的是子控件之间的交互,内部控件的测量与布局,都非常简单。在特殊的情况下,如子控件需要处理宽高和布局的时候,就交由 Behavior 内部的 onMeasureChild 与 onLayoutChild 方法来进行处理:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 生成节点依赖关系的有向无环图
prepareChildren();
ensurePreDrawListener();
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
final int paddingRight = getPaddingRight();
final int paddingBottom = getPaddingBottom();
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final boolean isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
final int widthPadding = paddingLeft + paddingRight;
final int heightPadding = paddingTop + paddingBottom;
int widthUsed = getSuggestedMinimumWidth();
int heightUsed = getSuggestedMinimumHeight();
int childState = 0;
final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);
// 按照拓扑排序的顺序来获取子 View
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
......
int childWidthMeasureSpec = widthMeasureSpec;
int childHeightMeasureSpec = heightMeasureSpec;
if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) {
// We're set to handle insets but this child isn't, so we will measure the
// child as if there are no insets
final int horizInsets = mLastInsets.getSystemWindowInsetLeft()
+ mLastInsets.getSystemWindowInsetRight();
final int vertInsets = mLastInsets.getSystemWindowInsetTop()
+ mLastInsets.getSystemWindowInsetBottom();
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
widthSize - horizInsets, widthMode);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
heightSize - vertInsets, heightMode);
}
// Behavior 为空,或者 Behavior 的 onMeasureChild() 返回 false(表示不测量)
// 时,CoordinatorLayout 才会自己执行测量
final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
lp.leftMargin + lp.rightMargin);
heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin);
childState = View.combineMeasuredStates(childState, child.getMeasuredState());
}
final int width = View.resolveSizeAndState(widthUsed, widthMeasureSpec,
childState & View.MEASURED_STATE_MASK);
final int height = View.resolveSizeAndState(heightUsed, heightMeasureSpec,
childState << View.MEASURED_HEIGHT_STATE_SHIFT);
setMeasuredDimension(width, height);
}
其实就是在子 View 设置了 Behavior,并且重写 Behavior 内部的 onMeasureChild() 时返回 true,那么 CoordinatorLayout 就把测量该子 View 的工作交给 Behavior 的 onMeasureChild()。onLayout() 类似:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
// Behavior 不为空且 onLayoutChild() 返回 true 才由 Behavior 摆放,
// 否则由 CoordinatorLayout 摆放
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
4.子控件的事件拦截与响应
同理对于事件的拦截与处理, 如果子控件需要拦截并消耗事件,那么交给 Behavior 内部的 onInterceptTouchEvent 与 onTouchEvent 方法进行处理。
先看拦截,在 CoordinatorLayout 的 onInterceptTouchEvent() 中:
@Override
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) {
// 重置所有子 View 中 Behavior 的触摸状态,传 true 时给 Behavior 的 onInterceptTouchEvent()
// 传 ACTION_CANCEL,传 false 时给 onTouchEvent() 传 ACTION_CANCEL
resetTouchBehaviors(true);
}
// 具体执行拦截操作的方法,获取是否要进行拦截
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(true);
}
return intercepted;
}
看 performIntercept(),参数 type 有两个值 TYPE_ON_INTERCEPT 和 TYPE_ON_TOUCH,分别表示是在 onInterceptTouchEvent()、onTouchEvent() 中调用:
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
// mTempList1 是一个空的 ArrayList
final List<View> topmostChildList = mTempList1;
// getTopSortedChildren() 把 topmostChildList 按照 Z 值由大到小进行排序
getTopSortedChildren(topmostChildList);
// Let topmost child views inspect first
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();
// 如果已经决定要进行拦截,对于不是 ACTION_DOWN 的事件,就给当前 Behavior 对应类型的方法传 ACTION_CANCEL
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// Cancel all behaviors beneath the one that intercepted.
// If the event is "down" then we don't have anything to cancel yet.
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;
}
// 不拦截且 Behavior 不为空,就用 Behavior 回调 onInterceptTouchEvent 或 onTouchEvent
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;
}
// 哪个 Behavior 执行了拦截,就把 Behavior 所属的子 View 赋值给 mBehaviorTouchView
if (intercepted) {
mBehaviorTouchView = child;
}
}
// Don't keep going if we're not allowing interaction below this.
// Setting newBlock will make sure we cancel the rest of the behaviors.
final boolean wasBlocking = lp.didBlockInteraction();
final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
newBlock = isBlocking && !wasBlocking;
if (isBlocking && !newBlock) {
// Stop here since we don't have anything more to cancel - we already did
// when the behavior first started blocking things below this point.
break;
}
}
topmostChildList.clear();
return intercepted;
}
可以看到 CoordinatorLayout 把在判断是否拦截时,会遍历所有子 View 并看它们的 Behavior 是否要进行拦截(通过调用 Behavior 的 onInterceptTouchEvent()),如果子 View 需要拦截,那么它的 Behavior 的 onInterceptTouchEvent() 就返回 true。
再看事件响应:
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean cancelSuper = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
// ① mBehaviorTouchView 表示哪个子 View 的 Behavior 对事件进行了拦截(在前面的 performIntercept 中赋值的),
// mBehaviorTouchView != null 表示已经有子 View 的 Behavior 拦截了事件,② performIntercept(ev, TYPE_ON_TOUCH)
// 在 Behavior 的 onTouchEvent() 返回 true 时也会返回 true。① ② 两个条件成立一个就会进入 if 分发 onTouchEvent
if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
// Safe since performIntercept guarantees that
// mBehaviorTouchView != null if it returns true
final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
}
}
// Keep the super implementation correct
if (mBehaviorTouchView == null) {
// 如果没有子 View 拦截,就调用 onTouchEvent() 自己处理
handled |= super.onTouchEvent(ev);
} else if (cancelSuper) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
super.onTouchEvent(cancelEvent);
}
// 回收 cancelEvent 资源
if (cancelEvent != null) {
cancelEvent.recycle();
}
// 重置 Behavior 的触摸状态
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(false);
}
return handled;
}
如果在判断拦截时已经有了拦截事件的 Behavior,就把事件处理交给这个 Behavior,如果所有子 View 的 Behavior 都不进行拦截,就调用自己的 onTouchEvent(),自己处理。其实还是之前的思想,遍历子 View,如果子 View 的 Behavior 要拦截或者处理事件,就交由 Behavior 的 onInterceptTouchEvent() 或 onTouchEvent() 处理。
附上 CoordinatorLayout 事件分发流程图:
二、自定义 Behavior
举个简单例子,实现一个不论上滑还是下拉都是 Header 部分优先的 Demo,效果如下:
布局很简单,Header 只是一个 TextView,下面是一个 RecyclerView,二者都指定了各自的 Behavior:
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/design_default_color_primary_dark"
android:textStyle="bold"
android:textSize="20sp"
android:textColor="@color/white"
android:gravity="center"
android:text="Hello World!"
app:layout_behavior=".HeaderBehavior"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/recycler"
app:layout_behavior=".RecyclerBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
先实现 HeaderBehavior:
class HeaderBehavior : CoordinatorLayout.Behavior<View> {
private var mLayoutTop = 0
/**
* 上一次进行完滑动后,滑动到纵向的哪个位置,可以理解为
* 一个纵向的坐标,标记的是 Header 的 getTop() 的值,
* 在 [-headerHeight , 0] 之间
*/
private var mOffsetTopAndBottom = 0
constructor() : super()
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
override fun onLayoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int): Boolean {
// 一种比较偷懒的方法,让 CoordinatorLayout 去布局,并且返回 true 表示
// Behavior 进行布局
parent.onLayoutChild(child, layoutDirection)
mLayoutTop = child.top
return true
}
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: View,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean {
return axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
}
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout,
child: View,
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int
) {
// 先计算假如传入的 dy 如果都消费掉,偏移的位置
var offset = mOffsetTopAndBottom - dy
// 偏移范围,当 child 的 getTop() 为 0 时是初始状态,child
// 在屏幕顶部,不能再向下滑;当 child 的 getTop() 为 -child.height
// 时,child 的下边缘在屏幕顶部,不能再向上滑
val minOffset = -getChildScrollingRange(child)
val maxOffset = 0
if (offset < minOffset) {
offset = minOffset
} else if (offset > maxOffset) {
offset = maxOffset
}
// ViewCompat.offsetTopAndBottom(child, -mOffsetTopAndBottom + offset)
ViewCompat.offsetTopAndBottom(child, offset - (child.top - mLayoutTop))
// 本次滑动消费掉的滑动距离
consumed[1] = mOffsetTopAndBottom - offset
// 记录本次滑动到的位置
mOffsetTopAndBottom = offset
}
// 获取 Header 滑动的最大距离
private fun getChildScrollingRange(child: View?): Int {
return child?.height ?: 0
}
}
依赖于 TextView 的 RecyclerView 的 RecyclerBehavior 的实现就简单一些:
class RecyclerBehavior : CoordinatorLayout.Behavior<RecyclerView> {
override fun layoutDependsOn(parent: CoordinatorLayout, child: RecyclerView, dependency: View): Boolean {
// 传入的 child 与 dependency 都是 parent 的直接子 View,因此直接判断
// 被依赖的是不是 TextView 就好
return dependency is TextView
}
// OnPreDrawListener 传递 EVENT_PRE_DRAW,在绘制之前会回调一次
// onDependentViewChanged(),因此跟随依赖变化即可
override fun onDependentViewChanged(parent: CoordinatorLayout, child: RecyclerView, dependency: View): Boolean {
// 滑动距离是变化后的 dependency 的底部与还未变化的 child 的头部
ViewCompat.offsetTopAndBottom(child, dependency.bottom - child.top)
return false
}
}