CoordinatorLayout(二)—— 原理分析与自定义 Behavior

CoordinatorLayout是Android中用于实现复杂布局交互的组件,它通过Behavior支持子View之间的依赖交互和嵌套滑动。在onMeasure()方法中,CoordinatorLayout通过有向无环图建立子View的依赖关系,并在View变化时通过OnPreDrawListener和HierarchyChangeListener监听并处理依赖变化。Behavior的onDependentViewChanged()和onDependentViewRemoved()方法处理依赖子View的变化。在嵌套滑动中,CoordinatorLayout将事件转发给Behavior,实现多子View间的滑动交互。此外,Behavior还可以控制子View的测量与布局以及事件拦截与响应。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本节我们来介绍 CoordinatorLayout 的工作原理,如果对 CoordinatorLayout 的基本使用还不熟悉,可以先参考下基本使用篇 CoordinatorLayout(一)—— 基本使用

一、CoordinatorLayout 功能分析

CoordinatorLayout 可以看成是对嵌套滑动机制的应用或扩展,它有如下四大功能:

  1. 处理子控件之间依赖下的交互:CoordinatorLayout 的子控件可以依赖某一个子控件,根据该依赖的变化做出相应的改变
  2. 处理子控件之间的嵌套滑动:嵌套滑动机制本身只能是 NestedScrollingChild 与 NestedScrollingParent 以一对一的形式进行交互,但是 CoordinatorLayout 将其发扬光大为一对多。比如滑动一个 RecyclerView 时,其上方 AppBarLayout 与下方 BottomNavigationView 都可以做出相应的行为变化
  3. 处理子控件的测量与布局
  4. 处理子控件的事件拦截与响应

以上功能都是建立在 CoordinatorLayout 提供的一个 Behavior —— 定义子 View 之间交互行为的插件之上,其内部提供了相应方法来应对这四个不同的功能:

下面我们来看看 CoordinatorLayout 是如何借助 Behavior 实现上面 4 个功能的。

1.子控件之间的依赖交互

当 CoordinatorLayout 中的子 View 需要依赖于其他子 View 时,会涉及到两个问题:

  1. 依赖于谁?
  2. 被依赖的 View 发生变化(大小、位置)时,该子 View 如何应变?

Behavior 提供了如下三个方法来解决以上两个问题:

方法名参数方法介绍
layoutDependsOnCoordinatorLayout parent:第二个参数 child 的父容器。
V child:进行测试的子 View。
View dependency:可能被 child 依赖的 View。
确定所提供的子视图是否有另一个特定的同级视图作为布局依赖项。在响应布局请求时,将至少调用此方法一次。

如果该方法对于给定的 <child,dependency> 对返回 true,那么作为父容器的 parent 将总是先摆放 dependency 再摆放 child。
onDependentViewChangedCoordinatorLayout parent:第二个参数 child 的父容器。
V child:待处理的子 View。
View dependency:被依赖并且发生了变化的 View。
当被依赖的 dependency 发生变化(大小、位置)时,Behavior 会回调此方法以更新 child(正常的 layout 流程不会回调)。

一个 View 的依赖是由 layoutDependsOn() 决定的,或者 child 已经设置了另一个 View 作为它的锚点。

如果一个 Behavior 通过这个方法改变了 child 的布局,也需要在 onLayoutChild() 中重建正确的位置。

方法默认返回 false,如果 Behavior 改变了 child 的大小或位置,则返回 true。
onDependentViewRemovedCoordinatorLayout parent:第二个参数 child 的父容器。
V child:待处理的子 View。
View dependency:被依赖并且被移除的 View。
dependency 从它的父容器中被移除后,Behavior 会回调此方法以更新 child。

第一个方法可以帮助我们建立 CoordinatorLayout 内各个子 View 之间的依赖关系,下面看看依赖关系是如何建立的。

依赖关系的建立

假如让你想办法获取 CoordinatorLayout 内各个子 View 之间的依赖关系,你会怎样实现?我想首先要考虑好如下两个问题:

  1. 在什么时候去获取依赖关系
  2. 如何恰当的表示依赖关系

对于问题 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 是如何实现子控件之间的依赖交互的:

  1. onMeasure() 时遍历所有子 View,将子 View 之间的依赖关系添加到一个有向无环图中,并对其进行拓扑排序
  2. 当有 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);
        }
    }

从上述代码中可以看出:

  1. 能接收到嵌套滑动事件的只能是设置了 Behavior 的直接子 View,并且处于 GONE 状态或者已经接收过嵌套滑动事件的子 View 无法接收嵌套滑动事件
  2. 如果 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
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值