CoordinatorLayout 之深入理解

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 上篇在对 CoordinatorLayout 作了一些简单介绍,以了解 CoordinatorLayout 带来的一些特性和常见用途。本篇将对 CoordinatorLayout 的源码进行一些分析,以了解它的相关特性的运行原理,以及 Behavior 的执行过程。

上篇在对 CoordinatorLayout 作了一些简单介绍,以了解 CoordinatorLayout 带来的一些特性和常见用途。本篇将对 CoordinatorLayout 的源码进行一些分析,以了解它的相关特性的运行原理,以及 Behavior 的执行过程。

Android design library 版本:26.1.0。

刚打开 CoordinatorLayout 的源码看了一下,单这一个文件就三千多行,所以就不从头至尾地在这里讲了,而是在读源码之前先提出一些问题,然后再带着这些问题到源码中去寻找答案。
以下是我此刻想到的问题。

  1. Behavior,anchor, dodgeInsetEdges 等参数是如何解析处理的?
  2. CoordinatorLayout 是如何做到派发子 View 的变化的?
  3. Anchor 是如何实现的?
  4. DodgeInsetEdges 是如何实现的?
  5. Behavior 能做什么?

下面一个个来看这些问题。

Behavior,anchor,dodgeInsetEdges 等参数是如何解析处理的?

在 CoordinatorLayout.LayoutParams 中,有两个构造方法,LayoutParams(int width, int height)LayoutParams(Context context, AttributeSet attrs)。其中前者是用于在 Java 代码中 addView() 生成 LayoutParams 所用,这里不再赘述。另一个构造方法LayoutParams(Context context, AttributeSet attrs)是在 CoordinatorLayout 的 generateLayoutParams(AttributeSet attrs) 方法中调用的,也就是在 inflate 操作时调用的,并传入在 xml 中所设置的子 View 的属性。
在这个构造方法中,insetEdge和 dodgeInsetEdges 的属性的获取与平常用法并无不同。需要注意的是 anchor 和 behavior。

behavior

先来看 behavior 的解析代码:

            mBehaviorResolved = a.hasValue(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior);
            if (mBehaviorResolved) {
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_Layout_layout_behavior));
            }

也就是 LayoutParams 为这个属性定义了两个变量,一个是用于判断是否存在这个属性,另一个是这个属性被解析出来的结果。这里 static Behavior parseBehavior(Context context, AttributeSet attrs, String name) 方法是通过从属性中获取到的 behavior 名称,通过反射来查找所对应的类,然后调用它的构造方法来创建 behavior 对象的。它支持的名称有以下三种:

  • .开头,它会获取context的包名,也就是我们 app 的包名,并且拼接到名称获取到完整的类名。
  • 如果不是上面的情况,则判断是否包含了.,是的话则认为是完整的类名。
  • 如果不是以上的情况,则使用 CoordinatorLayout 所在包的包名来拼接,得到完整的类名。

对应代码如下:

        if (name.startsWith(".")) {
            // Relative to the app package. Prepend the app package name.
            fullName = context.getPackageName() + name;
        } else if (name.indexOf('.') >= 0) {
            // Fully qualified package name.
            fullName = name;
        } else {
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                    ? (WIDGET_PACKAGE_NAME + '.' + name)
                    : name;
        }

获取到名称之后,则通过反射获取它的参数为 (Context, AttributeSet) 的构造器,然后创建出 Behavior 实例。如果以上失败则抛出异常。
以上是 behavior 在 xml 中声明了的解析过程。

但是上篇讲到,我们在类中通过添加 @DefaultBehavior 注解也是可以为我们的 View 设置一个 behavior 的。这是在哪里处理的呢?
在上面提到的 parseBehavior(Context, AttributeSet, String) 方法下还有一个方法,正是用来解析默认的 Behavior 的,代码如下:

    LayoutParams getResolvedLayoutParams(View child) {
        final LayoutParams result = (LayoutParams) child.getLayoutParams();
        if (!result.mBehaviorResolved) {
            Class<?> childClass = child.getClass();
            DefaultBehavior defaultBehavior = null;
            while (childClass != null &&
                    (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
                childClass = childClass.getSuperclass();
            }
            if (defaultBehavior != null) {
                try {
                    result.setBehavior(
                            defaultBehavior.value().getDeclaredConstructor().newInstance());
                } catch (Exception e) {
                    Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
                            " could not be instantiated. Did you forget a default constructor?", e);
                }
            }
            result.mBehaviorResolved = true;
        }
        return result;
    }

也就是如果参数的 behavior 没有被解析,那它就会判断当前的类是否有 DefaultBehavior 注解,有的话就取注解里声明的 Behavior ,没有的话则一直往父类上找。找到之后通过反射创建出 Behavior 对象,并把 mBehaviorResolved 设为 true,表示 behavior 已经被解析。需要注意的是,在这里调用的 behavior 的构造方法是没有参数的构造方法。所以如果你的 behavior 实现想要支持在注解当中使用的话,还需要重写无参的构造方法。这个方法分别在以下三种情况当中被调用:

onMeasure(int, int) -> prepareChildren() -> getResolvedLayoutParams(View)
getDependencySortedChildren() -> prepareChildren() -> getResolvedLayoutParams(View)
onRestoreInstanceState(Parcelable) -> getResolvedLayoutParams(View)

不过,我并没有找到调用 getDependencySortedChildren() 的地方。

anchor

接下来看 anchor 。除去表示位置的 anchorGravity 暂且不谈,LayoutParams 为其定义了三个成员变量,分别是:

  • int mAnchorId = View.NO_ID;
  • View mAnchorView;
  • View mAnchorDirectChild;

其中 mAnchorView 指的是 mAnchorId 所对应的 View,如果这个 View 是 CoordinatorLayout 的直接子 View,则 mAnchorDirectChildmAnchorView 一样,否则,将往上找 mAnchorView 的 parent,一直找到 CoordinatorLayout 的直接子 View,赋值给 mAnchorDirectChild

LayoutParams(Context context, AttributeSet attrs)中只解析了 mAnchorId 的值,在 prepareChildren() 调用前面所说的 getResolvedLayoutParams(View) 解析了布局参数之后,再调用该布局参数对象的 findAnchorView(CoordinatorLayout, View),最后调用到 resolveAnchorView(final View forChild, final CoordinatorLayout parent) 方法,解析 anchor 并赋值给 mAnchorViewmAnchorDirectChild

prepareChildren

CoordinatorLayout 中定义了两个重要的成员变量,如下:

    private final List<View> mDependencySortedChildren = new ArrayList<>();
    private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();

prepareChildren() 方法中,对所有子 View 做一次遍历,找到所有子 View 之间的依赖(包括 anchor 和 behavior 所形成的依赖),建立起一个有向无环图,保存到 mChildDag。然后对这个图排序处理,让 mDependencySortedChildren 得到一个按所依赖节点由低至高排序的子 View 集合。

CoordinatorLayout 是如何做到派发子 View 的变化的?

CoordinatorLayout 派发子 View 的变化分三种情况,一是 ViewTreeObserver,二是 OnHierarchyChangeListener,三是嵌套滚动的时候。

ViewTreeObserver

CoordinatorLayout 的一个子类实现了 ViewTreeObserver.OnPreDrawListene 接口,这个接口是当一个 View 准备被绘制时的回调。CoordinatorLayout 在这里通知子 View 准备被绘制的事件,代码如下:

    class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
        }
    }

当 CoordinatorLayout 的onMeasure(int, int)被回调的时候,在 prepareChildren 执行之后,也就是子 View 之间的依赖解析完成之后,会调用 ensurePreDrawListener() 方法,这个方法会判断子 View 之间是否有依赖,如果有的话,确保当前给 ViewTreeObserver 添加上 OnPreDrawListener,如果没有依赖,则确保移除。代码如下:

        if (hasDependencies != mNeedsPreDrawListener) {
            if (hasDependencies) {
                addPreDrawListener();
            } else {
                removePreDrawListener();
            }
        }
    void addPreDrawListener() {
        if (mIsAttachedToWindow) {
            // Add the listener
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }

        // Record that we need the listener regardless of whether or not we're attached.
        // We'll add the real listener when we become attached.
        mNeedsPreDrawListener = true;
    }

而在 CoordinatorLayout 被附加到 window 也就是 onAttachedToWindow() 被调用的时候,确保需要设置 OnPreDrawListener的时候对 ViewTreeObserver 设置 OnPreDrawListener

        if (mNeedsPreDrawListener) {
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }

另外,在 onDetachedFromWindow() 方法则确保这个监听被移除,这里不再赘述。

对层级变化的监听

对层级变化的监听则比较简单,CoordinatorLayout 在构造方法中通过设置监听获取子 View 的变化,如下:

super.setOnHierarchyChangeListener(new HierarchyChangeListener());

所设置的监听只有在有子 View 被添加或移除的时候回调。这里只通知了子 View 被移除的事件。

嵌套滚动

CoordinatorLayout 实现了嵌套滚动的接口,当对应的 fling 或 scroll 方法被调用的时候,如果子 View 有 behavior,则通知滚动事件。

子 View 变化的事件处理

以上三种情况,都是调用到 onChildViewsChanged(@DispatchChangeEvent final int type) 方法,在这里统一处理。在这个方法中,会遍历 mDependencySortedChildren ,取出每一个子 View,然后处理 anchor,dodge,以及 behavior,下面细谈。

Anchor 是如何实现的?

我们把这个问题再分解为两个问题:

  1. layout 时的处理
  2. 子 View 发生改变时的处理

layout 时对 anchor 的处理

在 CoordinatorLayout 的 onLayout(boolean changed, int l, int t, int r, int b) 方法中,还是先遍历 mDependencySortedChildren,取出每一个子 View,看是否有behavior 并且在 behavior.onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) 的回调中是否已经进行处理了,如果没有,则调用自己的 onLayoutChild(View child, int layoutDirection) 方法。
onLayoutChild(View child, int layoutDirection) 是 CoordinatorLayout 对子 View 进行布局的实现。当一个子 View 有 anchorView 的时候,通过调用 getDescendantRect(anchor, anchorRect) 计算出这个 anchorView 在此 CoordinatorLayout 的位置,然后调用 getDesiredAnchoredChildRect(child, layoutDirection, anchorRect, childRect) 计算出这个子 view 应该在的位置。在这个方法中,实际上它计算过程是调用的 getDesiredAnchoredChildRectWithoutConstraints(View child, int layoutDirection, Rect anchorRect, Rect out, LayoutParams lp, int childWidth, int childHeight),在这里取出 gravity 以及 anchorGravity,根据它们及 anchorView 的区域来计算出子 View 的位置,最后调用 constrainChildRect(lp, out, childWidth, childHeight) 以确保它不会超出自己的 margin 以及 CoordinatorLayout 的 padding 的设置。计算出来之后,调用子 View 的 layout(int left, int top, int right, int bottom) 方法设置它的位置。流程如下图:

layoutChildWithAnchor

子 View 发生改变时的处理

上面讲了在 layout 的时候怎么计算出子 View 的位置,这相当于是 View 的主动设置。但是当一个 子 View 通过设置 anchor 使它的固定在另一个 View 上的时候,如果所固定的 View 的位置发生变化,我们也要让子 View 的位置跟着变化,这里就是在前面所提到的 final void onChildViewsChanged(@DispatchChangeEvent final int type) 方法中实现的。
在这一方法中,会遍历 mDependencySortedChildren,然后在这个循环里面,会再次遍历这个集合,看当前遍历中的这个 View 的 mAnchorDirectChild 是哪个,然后调用 offsetChildToAnchor(child, layoutDirection) 重新计算和设置当前 View 的位置。代码如下:

        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;
            }

            // Check child views before for anchor
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);

                if (lp.mAnchorDirectChild == checkChild) {
                    offsetChildToAnchor(child, layoutDirection);
                }
            }
            //...

offsetChildToAnchor(View child, int layoutDirection)的方法与前面类似,不同的是,当计算出来的结果是 View 的位置有发生变化时,会再回调它的 behavior 的 onDependentViewChanged(this, child, lp.mAnchorView) 方法。

dodgeInsetEdges 是如何实现的?

关于 dodgeInsetEdges 的实现,同样来看 onChildViewsChanged(@DispatchChangeEvent final int type) 方法。
在这个方法,使用了一个变量 Rect inset,在遍历时记录当前遍历过的 View 在各种方向上所覆盖的最大距离。同时,每个子 View 也会检查自己设置的躲避的方向上,与这一条边的距离是否小于 inset 上所记录的距离,如果小于,则说明自己会被覆盖,那么就进行位移。
举例,如下图:
dodgeInsetEdges

假定父布局 CoordinatorLayout 的大小为[0, 0, 320, 480],View A 设置了 insetEdges 的方向是所有方向,并且它的范围是[50, 60, 150, 180],那么底边与顶部边缘的距离就是 180,顶边与底部边缘的距离就是 420,以此类推,得到 inset的值为[150, 180, 270, 420]。
再定 View B 只设定了 insetEdges 的方向顶部,它的坐标是[100, 220, 200, 300],那么遍历到 View B 的时候就会计算出 inset的值为[150, 300, 270, 420]。
如果 View C 的坐标是[150, 370, 270, 470],并且只设定了躲避内嵌边缘方向为顶部,由于它的顶部到父布局顶边的距离是270,少于 inset.top 的 300 这个值,也就是顶部会被遮挡,那么就调用 setInsetOffsetY(View child, int offsetY) 方法对它进行位移,让它避免被遮挡,同时记录下所位移的值,从而实现 dodgeInsetEdges 的特性。

Behavior 能做些什么?

相比于 CoordinatorLayout 所定义的其他布局参数,Behavior 显得非常灵活和强大,特别适合各种酷炫(惨无人道)的交互的实现。以下来简单介绍一下 Behavior 经常使用的一些方法。

构造方法

        public Behavior();
        public Behavior(Context context, AttributeSet attrs);

在第二个构造方法中,能接收到子 View 的布局参数属性。这也就意味着,我们不但能在构造方法这里直接拿到子 View 的布局属性,还能够自定义一些属性来帮助实现 behavior 所需的一些交互。

触摸事件处理

        public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev);
        public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev);

这也表示,我们可以接管子 View 的触摸事件进行处理。诸如实现上拉下拉左拉右拉的各种效果,也不需要再去重写 FrameLayout 或实现 ViewGroup 来实现了,只需要实现 behavior 而不影响我们的布局。

依赖 View 处理

        public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency);
        public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency);
        public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency);

这里可以处理我们的 View 依赖于其他哪些子 View ,并且当所依赖的 View 发生改变时可以进行处理。像 anchor 这样的效果,也可以通过这里的方法来实现。

测量

        public boolean onMeasureChild(CoordinatorLayout parent, V child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed)

这里可以对子 View 进行测量,也就是我们不需要重写子 View 就可以干涉它的测量,设置它的测量宽高。

布局

        public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection)

这里可以对子 View 布局。效果类比上面。在实现类似于 BottomSheet 的上拉效果或其他各种拉的效果,通常会使用到这个方法来设置它的初始位置以及 layout 发生之后它应该在的位置。

嵌套滚动

        @Deprecated
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes);
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes, @NestedScrollType int type);
        @Deprecated
        public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes);
        public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
                @ScrollAxis int axes, @NestedScrollType int type);
        @Deprecated
        public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target);
        public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, @NestedScrollType int type);
        @Deprecated
        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                @NonNull View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed);
        public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                @NonNull View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
        @Deprecated
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed);
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
                @NestedScrollType int type);
        public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, float velocityX, float velocityY,
                boolean consumed);
        public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
                @NonNull V child, @NonNull View target, float velocityX, float velocityY);

CoordinatorLayout 实现了嵌套滚动的接口,这一系列方法是 CoordinatorLayout 在对应方法中的回调。通常在做一些类似于上拉下拉的效果,而子 View 又包含了有同样方法的滚动效果的 View 时,会重写这些方法来实现嵌套滚动。

其他方法暂且不述。

通过 behavior 的这些方法,基本上我们不需要通过去重写一个 View,就可以通过 behavior 来扩展和增强它的交互功能,比如上一篇提到的,不管是 FrameLayout 还是 ScrollView 或是其他布局,使用了一个 BottomSheetBehavior,它就可以实现 BottomSheetBehavior 的这种效果。

本篇对 CoordinatorLayout 的源码分析到这里,下篇开始将着重于 Behavior 的使用。

目录
相关文章
|
7月前
|
Android开发
Android自带的DrawerLayout和ActionBarDrawerToggle实现侧滑效果
Android自带的DrawerLayout和ActionBarDrawerToggle实现侧滑效果
48 0
|
7月前
|
Android开发
[Android]DrawerLayout滑动菜单+NavigationView
[Android]DrawerLayout滑动菜单+NavigationView
80 0
|
XML API Android开发
TabLayout-Android M新控件
TabLayout-Android M新控件
78 0
|
开发工具 Android开发
RecyclerView与CardView的使用(一)
RecyclerView与CardView的使用(一)
159 0
RecyclerView与CardView的使用(一)
RecyclerView与CardView的使用(二)
RecyclerView与CardView的使用(二)
124 0
RecyclerView与CardView的使用(二)
|
Android开发
CoordinatorLayout使用浅析
CoordinatorLayout使用浅析
223 0
CoordinatorLayout使用浅析
|
Android开发
DrawerLayout使用详解
DrawerLayout使用详解
331 0
|
Android开发
在项目中运用使用CoordinatorLayout
在 2015 年的 I/O 开发者大会上,Google 介绍了一个新的 Android Design Support Library,该库可以帮助开发者在应用上使用 meterial design。以前在自己公司的项目上,有用过,最近把这个库中的 CoordinatorLayout单独拿出来做了个小例子写篇博文,纯粹当成整理复习笔记,下次如果需求再碰到这个,直接用上 。
2477 0
|
容器
CoordinatorLayout之初步认识
CoordinatorLayout是2015 I/O大会发布的一种布局,它可以说是一个非常强大的FrameLayout,主要用于协调(Coordinate)子控件,来帮助实现它们之间的一些交互效果。
1163 0