CoordinatorLayout原理解析

请添加图片描述

CoordinatorLayout的behavior解析

在上篇文章一篇文章教你学会安卓的嵌套滑动中实现的嵌套滑动是子View和父View进行通信,若想实现和兄弟节点通信该怎么实现的,其实也不难,当父View拿到事件后,就可以遍历到子View想通信的节点,再把事件交给此兄弟节点则可实现,谷歌是很体谅我们这种小白的,知道让我们自己写会有BUG,因此谷歌封装了一套布局CoordinatorLayout

CoordinatorLayout协调者布局,可以协调子View的各种事件,先看效果:

三个View,一个可以拖动的View,一个根据拖动View的高度进行变色的View,一个跟随拖动ViewView

使用

上面的效果就是借助CoordinatorLayout实现的,先看如何使用,上面效果的布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <!-- 可拖动View -->
    <com.hbsd.mdviewdemo.view.TouchView
        android:layout_marginLeft="150dp"
        android:layout_marginTop="150dp"
        android:layout_width="100dp"
        android:background="@color/teal_700"
        android:layout_height="100dp"/>
    <!--  跟随View -->
    <TextView
        android:background="@color/black"
        android:layout_width="100dp"
        android:textColor="@color/white"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="跟随"
        app:layout_behavior=".behavior.BrotherFollowBehavior"/>
    <!--  变色View -->
    <TextView
        android:gravity="center"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:text="变色"
        app:layout_behavior=".behavior.BrotherChangeColorBehavior"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

不难发现跟随View变色View有一个陌生的属性则是app:layout_behavior,先说结论,联动效果就是此属性控制的 ,后续我们对其进行详细讲解。

可拖动View是自定义的,代码如下:

TouchView

class TouchView : View {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int)
            : super(context, attrs, defStyleAttr)

    private var mLastX = 0f
    private var mLastY = 0f

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        val action = event?.action
        when (action) {
            MotionEvent.ACTION_DOWN -> {
                mLastX = event.getRawX()
                mLastY = event.getRawY()
            }
            MotionEvent.ACTION_MOVE -> {
                val dx = (event.getRawX() - mLastX).toInt()
                val dy = (event.getRawY() - mLastY).toInt()
                ViewCompat.offsetTopAndBottom(this, dy)
                ViewCompat.offsetLeftAndRight(this, dx)
                mLastX = event.getRawX()
                mLastY = event.getRawY()
            }
            else -> {
            }
        }
        return true
    }

}

app:layout_behavior属性指定的值,也是需要我们自己自定义的,先把代码贴出来,目前只需要知道设置此属性就可以产生联动

自定义Behavior需要实现CoordinatorLayout.Behavior,其需要一个依赖的泛型,目前只需要知道他的两个方法

layoutDependsOnonDependentViewChanged

layoutDependsOn决定是否依赖

onDependentViewChanged决定怎么变化

两个自定义的Behavior如下:

跟随Viewbehavior

BrotherFollowBehavior

class BrotherFollowBehavior : CoordinatorLayout.Behavior<View> {
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)

    override fun layoutDependsOn(
        parent: CoordinatorLayout,
        child: View,
        dependency: View
    ): Boolean {
        //依赖滑动View,表示和滑动View产生联动
        return dependency is TouchView
    }

    //若layoutDependsOn返回为true,视图改变时就会回调此方法
    override fun onDependentViewChanged(
        parent: CoordinatorLayout,
        child: View,
        dependency: View
    ): Boolean {
        //改变当前View的位置
        child.y = (dependency.bottom + 20).toFloat()
        child.x = dependency.x
        return true
    }

}

变色Viewbehavior

BrotherChangeColorBehavior

class BrotherChangeColorBehavior : CoordinatorLayout.Behavior<View> {

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)

    private lateinit var mArgbEvaluator : ArgbEvaluator;
    init {
        mArgbEvaluator = ArgbEvaluator()
    }

    override fun layoutDependsOn(
        parent: CoordinatorLayout,
        child: View,
        dependency: View,
    ): Boolean {
        //依赖滑动View,表示和滑动View产生联动
        return dependency is TouchView
    }

    override fun onDependentViewChanged(
        parent: CoordinatorLayout,
        child: View,
        dependency: View,
    ): Boolean {
        //改变当前View的北京颜色
        val color = mArgbEvaluator.evaluate(dependency.y / parent.height, Color.WHITE, Color.BLACK) as Int
        child.setBackgroundColor(color)
        return false
    }
}

现在还不需要知道Behavior中方法的原理,只需要存在一个疑问,为什么这么写就能绑定依赖,为什么在onDependentViewChanged()中加入变化代码就可以生效 ,这也是本篇文章要解决的问题,接下来我们就对CoordinatorLayoutBehavior的原理进行解析,解决上述疑问。

在这开启一下上帝视角Behavior的功能还不仅仅是协调嵌套滑动,还有三个协调功能分别是子控件之间的相互依赖,子控件测量和布局,子控件事件拦截和响应

我们着重分析四个功能是如何实现的

子控件的依赖交互

什么是子控件的依赖交互呢,就是当一个View删除时或者状态改变,别的View可以感知的到 ,我们文章开头的效果就是使用此功能实现的,监听的是状态改变,诸如位置信息,背景颜色也是状态的一种。

在讲解之前必须提两个类ViewTreeObserverOnHierarchyChangeListener

两个监听器

ViewTreeObserver

ViewTreeObserver注册一个观察者来监听视图树,当视图树的布局、视图树的焦点、视图树将要绘制、视图树滚动等发生改变时,ViewTreeObserver都会收到通知,ViewTreeObserver不能被实例化,可以调用View.getViewTreeObserver()来获得。

ViewTreeObserver常用内部类:

内部类接口备注
ViewTreeObserver.OnPreDrawListener当视图树将要被绘制时,会调用的接口
ViewTreeObserver.OnGlobalLayoutListener当视图树的布局发生改变或者View在视图树的可见状态发生改变时会调用的接口
ViewTreeObserver.OnGlobalFocusChangeListener当一个视图树的焦点状态改变时,会调用的接口
ViewTreeObserver.OnScrollChangedListener当视图树的一些组件发生滚动时会调用的接口
ViewTreeObserver.OnTouchModeChangeListener当视图树的触摸模式发生改变时,会调用的接口

在使用时可以给ViewTreeObserver注册上述监听器,从而监听视图树的一举一动,此篇文章不ViewTreeObserver的运行原理进行分析,只需要知道作用即可

说到这,是不是对CoordinatorLayout子控件的依赖交互功能的实现有些思路了呢

CoordinatorLayout中注册ViewTreeObserver并添加某个监听器,从而监听到视图树的变化,再根据子View的依赖关系进行处理即可,现在不需要理解,后续会深入分析

OnHierarchyChangeListener

此监听器可以监听当前ViewGroupaddremove

public interface OnHierarchyChangeListener {
    
    //在将新子视图添加到父视图时调用
    void onChildViewAdded(View parent, View child);

    //当子视图从父视图中移除时调用
    void onChildViewRemoved(View parent, View child);
}

View之间的依赖交互基本就是靠上述两个类实现,后续着重分析CoordinatorLayout中如何绑定上述的两个监听器

两个监听器的绑定

本节我们分析两个监听器在什么地方进行绑定

OnHierarchyChangeListener的绑定

CoordinatorLayout的构造中绑定了OnHierarchyChangeListener

CoordinatorLayout#CoordinatorLayout

public CoordinatorLayout(@NonNull Context context, @Nullable AttributeSet attrs,
        @AttrRes int defStyleAttr) {
    ...
	//绑定HierarchyChangeListener,此监听器监听子View的增删
    super.setOnHierarchyChangeListener(new HierarchyChangeListener());
	...
}

HierarchyChangeListener

private class HierarchyChangeListener implements OnHierarchyChangeListener {
    HierarchyChangeListener() {
    }
	//添加View则触发add
    @Override
    public void onChildViewAdded(View parent, View child) {
        if (mOnHierarchyChangeListener != null) {
            mOnHierarchyChangeListener.onChildViewAdded(parent, child);
        }
    }
	//删除View则触发此remove
    @Override
    public void onChildViewRemoved(View parent, View child) {
        //最最最重要的方法,后续进行分析
        onChildViewsChanged(EVENT_VIEW_REMOVED);
        if (mOnHierarchyChangeListener != null) {
            mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
        }
    }
}

ViewTreeObserver监听器的绑定

ViewTreeObserver监听器的绑定和View的生命周期是存在关系的

View的生命周期

回顾View的生命周期:

ActivityonCreate,调用setContentView()解析xml,反射创建View

onResume中触发绘制流程,执行ViewRootImpl中的performTraversals方法

private void performTraversals() {
    ...
    host.dispatchAttachedToWindow(mAttachInfo, 0);
    ...
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    performLayout(lp, mWidth, mHeight);
    ...
    performDraw();
}

measure,layout,draw都是我们熟知的方法,在他们之前还有一个方法就是dispatchAttachedToWindow(),在dispatchAttachedToWindow()中则会执行一系列的生命周期方法,我们不对其进行深入研究,只需要关注生命周期中的一个重要的方法就是onAttachedToWindow(),此方法类似于ActivityonCreate(),只要没有被移除屏幕销毁,则onAttachedToWindow()只会执行一次,其职责和onCreate()相似,一般是做初始化处理。

开启上帝视角,分析onAttachedToWindow()

onAttachedToWindow()

CoordinatorLayout#onAttachedToWindow

@Override
public void onAttachedToWindow() {
    super.onAttachedToWindow();
    ...
    //第一次加载时不会执行if,不会执行绑定
    //但是这里存在思路的,ViewTreeObserver确实是存在的,只是目前没有创建和绑定监听器
    if (mNeedsPreDrawListener) {
        if (mOnPreDrawListener == null) {
            mOnPreDrawListener = new OnPreDrawListener();
        }
        final ViewTreeObserver vto = getViewTreeObserver();
        vto.addOnPreDrawListener(mOnPreDrawListener);
    }
	...
    mIsAttachedToWindow = true;
}

后续根据View的生命周期会执行onMeasure

onMeasure

onMeasure主要处理依赖图的初始化和ViewTreeObserver监听器的绑定

绑定ViewTreeObserver监听器

ViewTreeObserver的监听器在onMeasure方法中添加

CoordinatorLayout#onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //重要中的重要,构建依赖关系,图的创建
    prepareChildren();	//先去看下节 依赖的构建
    //给ViewTreeObserver添加监听器
    ensurePreDrawListener(); //看下1
 	...
}

1.CoordinatorLayout#ensurePreDrawListener

void ensurePreDrawListener() {
    boolean hasDependencies = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        //当前View是否存在依赖关系
        if (hasDependencies(child)) { //看下2
            hasDependencies = true;
            break;
        }
    }
	//mNeedsPreDrawListener初始为false,若存在依赖关系,hasDependencies为true,会命中if
    if (hasDependencies != mNeedsPreDrawListener) {
        if (hasDependencies) {
            //给ViewTreeObserver添加监听器
            addPreDrawListener(); //看下3
        } else {
            removePreDrawListener();
        }
    }
}

2.CoordinatorLayout#hasDependencies

private boolean hasDependencies(View child) {
    //在prepareChildren已经构建过图,此方法判断存不存在此child的依赖
    return mChildDag.hasOutgoingEdges(child);
}

3.CoordinatorLayout#addPreDrawListener

void addPreDrawListener() {
    //若执行过onAttachedToWindow()则mIsAttachedToWindow为true
    if (mIsAttachedToWindow) {
        // 绑定监听器
        if (mOnPreDrawListener == null) {
            mOnPreDrawListener = new OnPreDrawListener();
        }
        final ViewTreeObserver vto = getViewTreeObserver();
        vto.addOnPreDrawListener(mOnPreDrawListener);//mOnPreDrawListener为OnPreDrawListener 看下面分析
    }
    mNeedsPreDrawListener = true;
}

OnPreDrawListener

class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
    @Override
    public boolean onPreDraw() {
        //和OnHierarchyChangeListener的remove方法类似,只是调用时传入的参数不同,绘制时就会触发此方法
        onChildViewsChanged(EVENT_PRE_DRAW);
        return true;
    }
}

总结:在onMeasure中初始化子View的依赖关系,是否依赖取决于是否锚点和BehaviorlayoutDependsOn方法,紧接着会绑定ViewTreeObserver监听器,去监听视图树的一举一动

依赖的构建

依赖的构建全部依靠DirectedAcyclicGraph,数据结构为 有向无环图,采用邻接表实现 ,不对其进行深入研究,只需要知道依赖关系使用此类完成,其提供创建边和节点的函数。

CoordinatorLayout#prepareChildren

private void prepareChildren() {
    mDependencySortedChildren.clear();
    mChildDag.clear();

    for (int i = 0, count = getChildCount(); i < count; i++) {
        //获取一个子View
        final View view = getChildAt(i);
		//将Behavior正确赋值,先看下节Behavior的创建,再回来看下面方法 
        final LayoutParams lp = getResolvedLayoutParams(view);//从(Behavior的创建)小节回来以后,看下1
        
        //设置锚点,可看下下节(锚点的创建),不要跟丢了流程,回顾一下我们为什么看此节,是因为在绑定监听器之前需要先构建View之间的依赖
        lp.findAnchorView(this, view);
		
        //图中添加节点
        mChildDag.addNode(view);

        //第二重循环,找到第一重循环拿到View所有依赖的View
        for (int j = 0; j < count; j++) {
            //跳过当前View
            if (j == i) {
                continue;
            }
            final View other = getChildAt(j);
            //判断view(第一重循环的View)是否依赖于other,看下2
            if (lp.dependsOn(this, view, other)) {
                if (!mChildDag.contains(other)) {
                    // Make sure that the other node is added
                    mChildDag.addNode(other);
                }
                // Now add the dependency to the graph
                mChildDag.addEdge(other, view);
            }
        }
    }

    //将依赖进行排序
    mDependencySortedChildren.addAll(mChildDag.getSortedList());
    //因为上述两重循环处理中,是一个View去找它的所有的依赖View,但是最终我们希望集合开头是不依赖任何View的View,因此进行反转
    Collections.reverse(mDependencySortedChildren);
}

1.CoordinatorLayout#getResolvedLayoutParams

LayoutParams getResolvedLayoutParams(View child) {
    //获取当前View的LayoutParams
    final LayoutParams result = (LayoutParams) child.getLayoutParams();
    //如果在XML中未指定Behavior属性则命中if
    if (!result.mBehaviorResolved) {
        //若child本身实现了AttachedBehavior,则将Behavior赋值到LayoutParams中
        if (child instanceof AttachedBehavior) {
            Behavior attachedBehavior = ((AttachedBehavior) child).getBehavior();
            if (attachedBehavior == null) {
                Log.e(TAG, "Attached behavior class is null");
            }
            result.setBehavior(attachedBehavior);
            result.mBehaviorResolved = true;
        } else {
            //根据注释确定Behavior,已弃用,了解即可
            
            //获取当前child的类
            Class<?> childClass = child.getClass();
            DefaultBehavior defaultBehavior = null;
            //往上寻找有没有被注解的类,直到找到一个存在注解的或者childClass为null
            while (childClass != null
                    && (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class))
                    == null) {
                childClass = childClass.getSuperclass();
            }
            //如果找到了defaultBehavior则设置Behavior
            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;
}

2.LayoutParams#dependsOn

boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency == mAnchorDirectChild //若dependency == 锚点则也算做依赖,举个例子若FloatingActionButton锚点AppBarLayout中的TextView,那么FloatingActionButton会和AppBarLayout建立依赖
            || shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))	//检查具有此 LayoutParams 的视图是否应该避开指定的视图
            || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));//调用mBehavior的layoutDependsOn判断是否依赖,这也是三种情况中最常见的依赖情况,这是我们接触的Behavior的第一个方法,作用是决定两个View是否依赖
}

总结:子View的依赖关系通过prepareChildren()方法建立,内部是图的数据结构,判断两个View是否依赖主要通过锚点和Behavior的layoutDependsOn方法。

Behavior的创建

Behavior的官方定义为CoordinatorLayout 子视图的交互行为插件。行为实现了用户可以对子视图进行的一个或多个交互。这些交互可能包括拖拽、滑动、甩动或任何其他手势。

笔者是这么理解的,假设ViewA指定和ViewB交互,触发协调事件会先通知到ViewA的父(CoordinatorLayout),父通过调用ViewA中指定的Behavior的方法来实现想要的效果。也就是说ViewAViewB是通过Behavior来协调的。

目前先不分析Behavior中的方法,只需要知道它的作用即可,后续我们会分析Behavior中的方法

Behavior属性是保存在LayoutParams中的,下面分析一下他是怎么创建的

熟悉View创建流程的读者应该知道,View是在LayoutInflater#rInflate方法中创建的

LayoutInflater#rInflate

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
	...
    //假设创建的为CoordinatorLayout的子View,那么parent就是CoordinatorLayout
    final View view = createViewFromTag(parent, name, context, attrs);
    final ViewGroup viewGroup = (ViewGroup) parent;
    //generateLayoutParams执行的为CoordinatorLayout的,看下面分析
    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
    //递归创建子的子View
    rInflateChildren(parser, view, attrs, true);
    //添加此次创建的View
    viewGroup.addView(view, params);
    ...
}

CoordinatorLayout#generateLayoutParams

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

CoordinatorLayout#LayoutParams

public static class LayoutParams extends MarginLayoutParams {
	//Behavior的声明
    Behavior mBehavior;
    //Behavior是否创建的标志位
    boolean mBehaviorResolved = false;

   	...
	//锚点,锚点也是是否依赖的一个因素,关于锚点的作用在 锚点View的创建小节中解释
    View mAnchorView;
    View mAnchorDirectChild;

  	...

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.CoordinatorLayout_Layout);
        ...
        //获取当前View的锚点View的id
        mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
                View.NO_ID);
        //获取锚点方向
        this.anchorGravity = a.getInteger(
                R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
                Gravity.NO_GRAVITY);
        ...
		//当前View是否包含layout_behavior属性
        mBehaviorResolved = a.hasValue(
                R.styleable.CoordinatorLayout_Layout_layout_behavior);
        if (mBehaviorResolved) {
            //通过反射创建Behavior 看下面分析
            mBehavior = parseBehavior(context, attrs, a.getString(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior));
        }
        ...

        if (mBehavior != null) {
      		//告诉Behavior绑定成功
            mBehavior.onAttachedToLayoutParams(this);
        }
    }
}

CoordinatorLayout#parseBehavior

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    if (TextUtils.isEmpty(name)) {
        return null;
    }
	//获取完整的名字
    final String fullName;
    if (name.startsWith(".")) {
        fullName = context.getPackageName() + name;
    } else if (name.indexOf('.') >= 0) {
        fullName = name;
    } else {
        fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
            ? (WIDGET_PACKAGE_NAME + '.' + name)
            : name;
    }

    try {
        //static final ThreadLocal<Map<String, Constructor<Behavior>>> sConstructors = new ThreadLocal<>();
        //sConstructors为ThreadLocal,内部的map是Behavior构造的缓存
        //此处获取到Behavior的缓存
        Map<String, Constructor<Behavior>> constructors = sConstructors.get();
        //若此时还未创建过,则创建
        if (constructors == null) {
            constructors = new HashMap<>();
            //将map交与ThreadLocal
            sConstructors.set(constructors);
        }
        //用全限定名拿到map的之前Behavior的缓存
        Constructor<Behavior> c = constructors.get(fullName);
        //若为null,则通过反射获取到相应的Behavior的构造,并将此构造放入缓存
        if (c == null) {
            final Class<Behavior> clazz =
                (Class<Behavior>) Class.forName(fullName, false, context.getClassLoader());
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        //创建新的Behavior
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}

总结:在解析xml时创建LayoutParamsLayoutParams中会获取xml中指定的值,通过路径进行反射创建,本节完毕回到依赖的构建小节继续往下分析

锚点View的创建

锚点View是做啥的,直接看效果:

看右边的按钮,这个按钮是锚点在Bar的右下角的

<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="16dp"
    android:src="@drawable/ic_comment"
    app:layout_anchor="@id/appBar"
    app:layout_anchorGravity="bottom|end" />

本篇文章不对material design控件进行解释,只需要知道我们将此按钮锚点在了AppBarLayout上,当AppBarLayout缩小时Button的状态也会发生改变,此功能也是依赖于CoordinatorLayout来实现的

在创建FloatingActionButtonLayoutParams时会初始化LayoutParams中的两个属性,下面代码为CoordinatorLayout#LayoutParams的构造方法

//获取当前View的锚点View的id
mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
                            View.NO_ID);
//获取锚点方向
this.anchorGravity = a.getInteger(
    R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
    Gravity.NO_GRAVITY);

在后续创建依赖时,在onMeasureprepareChildren中对其合法性进行判断

CoordinatorLayout#prepareChildren

for (int i = 0, count = getChildCount(); i < count; i++) {
    lp.findAnchorView(this, view);
}

LayoutParams#findAnchorView

View findAnchorView(CoordinatorLayout parent, View forChild) {
    //若没有指定View,则直接返回
    if (mAnchorId == View.NO_ID) {
        mAnchorView = mAnchorDirectChild = null;
        return null;
    }
	//若mAnchorView不为null,则判断其是否合法,下面方法主要寻找CoordinatorLayout的锚点的直接布局
    //在LayoutParams中有两个属性mAnchorView,mAnchorDirectChild
    //mAnchorView的代表真正需要锚点的View
    //mAnchorDirectChild的mAnchorView的上层View,是CoordinatorLayout的直系View
    //比如说FloatingActionButton锚点AppBarLayout中的TextView,那么mAnchorView则为TextView,mAnchorDirectChild为AppBarLayout
    //如此设计的道理也是很简单的,因为锚点到了某个布局的内部,如果CoordinatorLayout不知道它要协调的直系儿子,那么他怎么协调
    if (mAnchorView == null || !verifyAnchorView(forChild, parent)) {//看下1
        //若之前没有初始化过mAnchorView,那么则在下面方法中初始化
        resolveAnchorView(forChild, parent);//看下2
    }
    return mAnchorView;
}

1.LayoutParams#verifyAnchorView

private boolean verifyAnchorView(View forChild, CoordinatorLayout parent) {
    //若id不一致则为不合法
    if (mAnchorView.getId() != mAnchorId) {
        return false;
    }
	//directChild最终指向CoordinatorLayout的直接儿子
    View directChild = mAnchorView;
    //若p不是CoordinatorLayout,就继续往上找
    for (ViewParent p = mAnchorView.getParent();
            p != parent;
            p = p.getParent()) {
        //如果p最终为null或者锚点的View就是自己的祖宗,则锚点是无效的
        if (p == null || p == forChild) {
            mAnchorView = mAnchorDirectChild = null;
            return false;
        }
        if (p instanceof View) {
            directChild = (View) p;
        }
    }
    mAnchorDirectChild = directChild;
    return true;
}

2.LayoutParams#resolveAnchorView

private void resolveAnchorView(final View forChild, final CoordinatorLayout parent) {
    //找到在xml中声明的layout_anchor的View
    mAnchorView = parent.findViewById(mAnchorId);
    //若此View不为null
    if (mAnchorView != null) {
        //若锚点View是CoordinatorLayout,且CoordinatorLayout处于编辑模式则此锚点无效
        if (mAnchorView == parent) {
            if (parent.isInEditMode()) {
                mAnchorView = mAnchorDirectChild = null;
                return;
            }
            throw new IllegalStateException(
                    "View can not be anchored to the the parent CoordinatorLayout");
        }
		//寻找直接子View,跟上述1中的寻找差不多
        View directChild = mAnchorView;
        for (ViewParent p = mAnchorView.getParent();
                p != parent && p != null;
                p = p.getParent()) {
            if (p == forChild) {
                if (parent.isInEditMode()) {
                    mAnchorView = mAnchorDirectChild = null;
                    return;
                }
                throw new IllegalStateException(
                        "Anchor must not be a descendant of the anchored view");
            }
            if (p instanceof View) {
                directChild = (View) p;
            }
        }
        mAnchorDirectChild = directChild;
    } else {
        ...
    }
}

总结LayoutParams的构造中获取两个属性分别是锚点Viewid和锚点的方向,在依赖构建prepareChildren方法中会初始化两个属性,mAnchorViewmAnchorDirectChildmAnchorDirectChildmAnchorView父View,是CoordinatorLayout的直接子View,这里一定要记清楚,锚点View是某个需要锚点ViewLayoutParams的一个属性,这和CoordinatorLayout没有关系,分析完毕回到依赖的构建小节继续往下分析。

依赖交互的实现

上述讲了这么多,都是前置内容,本小节则对依赖交互的实现进行解析

上述我们知道了依赖关系的构建,依赖信息最终都保存在了mDependencySortedChildren集合中

在此之前绑定了两个监听器OnHierarchyChangeListenerOnPreDrawListener

回顾一下两个监听器的作用

当删除View时触发

HierarchyChangeListener#onChildViewRemoved

@Override
public void onChildViewRemoved(View parent, View child) {
    //传入删除的标志位,处理删除事件,非常非常重要的方法
    onChildViewsChanged(EVENT_VIEW_REMOVED);
	//不设置则不执行,一般也不会设置
    if (mOnHierarchyChangeListener != null) {
        mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
    }
}

当视图树绘制时触发:

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

都会触发一个方法,onChildViewsChanged()这也是最最重要的方法,下面我们对此方法进行分析

CoordinatorLayout#onChildViewsChanged

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    ...
    
	//遍历全部有依赖关系的子View,集合中的元素是后边依赖于前面的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) {
            // 若没有绘制或者不可见则跳过
            continue;
        }

        // 检查是否存在锚点View,遍历i之前View,为什么是之前,因为集合是排序了的,永远都是后面依赖前面
        for (int j = 0; j < i; j++) {
            final View checkChild = mDependencySortedChildren.get(j);
			//之前在锚点View的创建小节中知道mAnchorDirectChild的作用,忘记的读者看前面的内容,若找到直接依赖则命中if
            if (lp.mAnchorDirectChild == checkChild) {
                //处理锚点更新,因为锚点的意思是将某个View固定到某个View的某处,这里需要保持View的锚点View的相对距离不变,下面方法则是处理相对位置
                offsetChildToAnchor(child, layoutDirection); //感兴趣可以看(锚点View的偏移处理)小节,不感兴趣可跳过
            }
        }

       ...
           
        // 往后查找,后边的View依赖前面的View,现在寻找后面依赖于当前View的View
        for (int j = i + 1; j < childCount; j++) {
            final View checkChild = mDependencySortedChildren.get(j);
            final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
            final Behavior b = checkLp.getBehavior();
			//若layoutDependsOn返回true则代表依赖,命中if
            if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                //若是ViewTreeObserver回调的当前方法,则传入EVENT_PRE_DRAW
                if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                    //如果这是来自预绘制并且我们已经从嵌套滚动中更改,则跳过调度并重置标志
                    checkLp.resetChangedAfterNestedScroll();
                    continue;
                }
				
                final boolean handled;
                switch (type) {
                    //只有OnHierarchyChangeListener回调时会命中此case
                    case EVENT_VIEW_REMOVED:
                        //本篇文章Behavior的第二个方法,作用是处理删除View时的回调
                        b.onDependentViewRemoved(this, checkChild, child);
                        handled = true;
                        break;
                    //其他情况都会走下面case
                    default:
                        // 本篇文章Behavior的第三个方法,作用是通知Behavior依赖的View状态发生了变化,不同的业务有不同的实现效果
                        handled = b.onDependentViewChanged(this, checkChild, child);
                        break;
                }

                if (type == EVENT_NESTED_SCROLL) {
                    // 如果是嵌套滑动则设置标志位
                    checkLp.setChangedAfterNestedScroll(handled);
                }
            }
        }
    }

    releaseTempRect(inset);
    releaseTempRect(drawRect);
    releaseTempRect(lastDrawRect);
}

总结:只要视图树要绘制或者有删除View的操作,都会回调onChildViewsChanged()方法,在此方法中第一步处理锚点,第二步则会根据传入的type处理其他依赖,type右三种分别是

static final int EVENT_PRE_DRAW = 0; //绘制前触发,来自于OnPreDrawListener的回调
static final int EVENT_NESTED_SCROLL = 1;	//嵌套滑动触发
static final int EVENT_VIEW_REMOVED = 2;	//HierarchyChangeListener的remove回调触发

锚点View的偏移处理

上述我们知道锚点也属于依赖交互的一种,此小节分析锚点时怎么生效的,下面方法是在onChildViewsChanged()中调用的,职责就是移动相应的View,固定锚点。

CoordinatorLayout#offsetChildToAnchor

void offsetChildToAnchor(View child, int layoutDirection) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (lp.mAnchorView != null) {
        //锚点View的Rect声明,此时为空,安卓的常见操作在缓存里获取空Rect
        final Rect anchorRect = acquireTempRect();
        //当前View的Rect声明
        final Rect childRect = acquireTempRect();
        //最终View的位置信息的Rect声明
        final Rect desiredChildRect = acquireTempRect();
		//不用再往里看,只是获取lp.mAnchorView的位置信息,并给anchorRect赋值
        getDescendantRect(lp.mAnchorView, anchorRect);
        //不用再往里看,只是获取当前View的位置信息,并给childRect赋值
        getChildRect(child, false, childRect);
		//获取子View的宽高
        int childWidth = child.getMeasuredWidth();
        int childHeight = child.getMeasuredHeight();
        //此方法初始化desiredChildRect,看下1
        getDesiredAnchoredChildRectWithoutConstraints(child, layoutDirection, anchorRect,
                desiredChildRect, lp, childWidth, childHeight);
        //若最终View,左和左不等或者上和上不等则意味着发生了改变
        boolean changed = desiredChildRect.left != childRect.left ||
                desiredChildRect.top != childRect.top;
        //最终的位置信息可能是越界无效的,在此方法中更正错误的位置信息,看下2
        //无效举例:假设锚点左上角,若指定layout_anchorGravity为left,top,且layout_gravity也指定为left,top此时按照对getDesiredAnchoredChildRectWithoutConstraints方法的分析,此时View的left = -childWidth,top = -childHeight,right = 0, bottom = 0,View正好在屏幕外,下面函数就能解决这个问题
        constrainChildRect(lp, desiredChildRect, childWidth, childHeight);
		//计算滑动的距离
        final int dx = desiredChildRect.left - childRect.left;
        final int dy = desiredChildRect.top - childRect.top;
		//开始滑动
        if (dx != 0) {
            ViewCompat.offsetLeftAndRight(child, dx);
        }
        if (dy != 0) {
            ViewCompat.offsetTopAndBottom(child, dy);
        }
		//只要改变了,则回调Behavior的onDependentViewChanged,根据业务进行下一步操作,此方法我们自己指定或者实现的
        if (changed) {
            // If we have needed to move, make sure to notify the child's Behavior
            final Behavior b = lp.getBehavior();
            if (b != null) {
                b.onDependentViewChanged(this, child, lp.mAnchorView);
            }
        }
		//释放Rect
        releaseTempRect(anchorRect);
        releaseTempRect(childRect);
        releaseTempRect(desiredChildRect);
    }
}

1.CoordinatorLayout.getDesiredAnchoredChildRectWithoutConstraints

private void getDesiredAnchoredChildRectWithoutConstraints(View child, int layoutDirection,
        Rect anchorRect, Rect out, LayoutParams lp, int childWidth, int childHeight) {
    //获取layout_gravity属性的值
    final int absGravity = GravityCompat.getAbsoluteGravity(//根据相对重心信息获取绝对重心位置
            resolveAnchoredChildGravity(lp.gravity), layoutDirection);//先获取相对重心位置
    final int absAnchorGravity = GravityCompat.getAbsoluteGravity(
            resolveGravity(lp.anchorGravity),	
            layoutDirection);
	//只需要记住,若指定gravity则一定会包含指定的信息,若没有指定gravity,会有默认值
    //layout_gravity默认为CENTER
    //layout_anchorGravity默认会根据当前的阅读方式来决定,如果是LTR,则会指定为left|top,若RIL,则会指定为RTL
    final int hgrav = absGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
    final int vgrav = absGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int anchorHgrav = absAnchorGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
    final int anchorVgrav = absAnchorGravity & Gravity.VERTICAL_GRAVITY_MASK;
	//左上角坐标信息
    int left;
    int top;
	
	//定位水平坐标,View和锚点View的相对位置信息不能改变
    switch (anchorHgrav) {
        default:
        case Gravity.LEFT:
            left = anchorRect.left;
            break;
        case Gravity.RIGHT:
            left = anchorRect.right;
            break;
        case Gravity.CENTER_HORIZONTAL:
            left = anchorRect.left + anchorRect.width() / 2;
            break;
    }
	//定位纵坐标
    switch (anchorVgrav) {
        default:
        case Gravity.TOP:
            top = anchorRect.top;
            break;
        case Gravity.BOTTOM:
            top = anchorRect.bottom;
            break;
        case Gravity.CENTER_VERTICAL:
            top = anchorRect.top + anchorRect.height() / 2;
            break;
    }

    // 若View指定了layout_gravity,默认走CENTER,居中
    switch (hgrav) {
        default:
        case Gravity.LEFT:
            left -= childWidth;
            break;
        case Gravity.RIGHT:
            // Do nothing, we're already in position.
            break;
        case Gravity.CENTER_HORIZONTAL:
            left -= childWidth / 2;
            break;
    }

    switch (vgrav) {
        default:
        case Gravity.TOP:
            top -= childHeight;
            break;
        case Gravity.BOTTOM:
            // Do nothing, we're already in position.
            break;
        case Gravity.CENTER_VERTICAL:
            top -= childHeight / 2;
            break;
    }
	//设置当前View应该在的新位置
    out.set(left, top, left + childWidth, top + childHeight);
}

2.CoordinatorLayout.constrainChildRect

private void constrainChildRect(LayoutParams lp, Rect out, int childWidth, int childHeight) {
    final int width = getWidth();
    final int height = getHeight();
    //拿到较大大的,让View尽量往右下角偏移
    int left = Math.max(getPaddingLeft() + lp.leftMargin, //由于存在getPaddingLeft()的兜底,永远不可能超出边界
            //从左边界和从右往左间过去的边界中取一个相对靠左的
            Math.min(out.left,
                    width - getPaddingRight() - childWidth - lp.rightMargin));
    //分析和上面差不多
    int top = Math.max(getPaddingTop() + lp.topMargin,
            Math.min(out.top,
                    height - getPaddingBottom() - childHeight - lp.bottomMargin));

    out.set(left, top, left + childWidth, top + childHeight);
}

总结:ViewTreeObserver绑定OnPreDrawListener监听器,在视图滚动时,最终会调用drawdraw会回调监听器中方法onChildViewsChanged,此时measurelayout已经执行完毕,所以锚点ViewRect已经发生变化,而当前View还是之前未滚动锚点View的位置数据,与新的锚点View的位置信息不同,因此会判定需要改变,最终发生滑动并回调BehavioronDependentViewChanged方法,让程序员进行自己的变化业务处理。此时回到依赖交互的实现小节,在处理锚点的地方继续往下分析。

文章开头的效果的原理解析

先回到文章开头的使用小节,把代码通读一遍,试着自己理解原理(5分钟时间)。时间到,不理解也没有关系,下面对其原理进行解释。

因为我们在XML中给跟随View指定了BrotherFollowBehavior,给变色View指定了BrotherChangeColorBehavior

两个BehaviorlayoutDependsOn()方法是一样的,方法如下:

//读者没有忘记此方法的作用吧,此方法决定两个View是否存在依赖关系,在构建图的时候调用了此fang'fa
override fun layoutDependsOn(
    parent: CoordinatorLayout,
    child: View,
    dependency: View,
): Boolean {
    //都依赖于TouchView
    return dependency is TouchView
}

无论是跟随还是变色,最终都会触发绘制,只要触发绘制,ViewTreeObserver绑定的OnPreDrawListener中的onPreDraw方法就一定会执行,又因为onPreDraw调用了onChildViewsChanged()方法,onChildViewsChanged()会遍历处理所有存在依赖的View,挨个回调BehavioronDependentViewChanged方法

我们再看两种BehavioronDependentViewChanged方法的实现

BrotherChangeColorBehavior#onDependentViewChanged

override fun onDependentViewChanged(
    parent: CoordinatorLayout,
    child: View,
    dependency: View,
): Boolean {
	//根据依赖的View的高度改变颜色
    val color = mArgbEvaluator.evaluate(dependency.y / parent.height, Color.WHITE, Color.BLACK) as Int
    child.setBackgroundColor(color)
    return false
}

BrotherFollowBehavior#onDependentViewChanged

override fun onDependentViewChanged(
    parent: CoordinatorLayout,
    child: View,
    dependency: View
): Boolean {
    //根据所依赖的View的位置改变位置
    child.y = (dependency.bottom + 20).toFloat()
    child.x = dependency.x
    return true
}

说到这开头的效果的原理就很清晰了。

使用时要注意的问题

只要触发View的整套绘制流程,就要重建依赖图,并遍历所有存在依赖的View,回调其Behavior中的方法,假设说CoordinatorLayout存在的依赖过多,势必会造成卡顿

我们在回想依赖由什么决定

主要是两个方式

  • BehaviorlayoutDependsOn(),由layout_behavior属性确定
  • 锚点的指定,由layout_anchor属性确定

因此这两个属性不易过多使用

子控件协调嵌套滑动

CoordinatorLayout继承了NestedScrollingParent2和NestedScrollingParent3

嵌套滑动在笔者的另一篇文章中已经分析过,详情请看一篇文章教你学会安卓的嵌套滑动

在这篇文章中默认读者掌握嵌套滑动的相关知识,直接分析原理

NestedScrollingParent中有7个方法,CoordinatorLayout的实现如下:

NestedScrollingParent中的方法实现

CoordinatorLayout#onStartNestedScroll

public boolean onStartNestedScroll(View child, View target, int axes, int type) {
    boolean handled = false;
	
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        //判断View是否可见,不可见则下一个
        if (view.getVisibility() == View.GONE) {
            // If it's GONE, don't dispatch
            continue;
        }
        //获取到当前View的LayoutParams
        final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 
        //获取Behavior
        final Behavior viewBehavior = lp.getBehavior();
        //Behavior不为null,则需要回调
        if (viewBehavior != null) {
            //回调onStartNestedScroll,本篇文章出现的Behavior的第四个方法
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                    target, axes, type);
            handled |= accepted;
            //设置此次嵌套滑动的事件的类型,只有为accepted为true后续的事件才可以消费
            lp.setNestedScrollAccepted(type, accepted);
        } else {
            lp.setNestedScrollAccepted(type, false);
        }
    }
    return handled;
}

CoordinatorLayout#onNestedScrollAccepted

//省略一些非空判断,重点在调用哪个回调
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes, int type) {
    //在之前的文章中讲过,不再赘述
    mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type);
    ...
	
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        ...
        //上个方法的末尾调用了setNestedScrollAccepted,若此方法的第二个参数为false,则不会相应此次嵌套滑动
        if (!lp.isNestedScrollAccepted(type)) {
            continue;
        }

        ...
        if (viewBehavior != null) {
            //回调onNestedScrollAccepted,本篇文章出现的Behavior的第五个方法
            viewBehavior.onNestedScrollAccepted(this, view, child, target,
                    nestedScrollAxes, type);
        }
    }
}

CoordinatorLayout#onNestedPreScroll

//省略非空判断
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 Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            //初始化消费的举例
            mBehaviorConsumed[0] = 0;
            mBehaviorConsumed[1] = 0;
            //回调onNestedPreScroll()方法,给mBehaviorConsumed赋值,说明消费了举例,本篇文章出现的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;
        }
    }
	//记录消费的距离,等待方法执行结束,传回触发嵌套滑动的View
    consumed[0] = xConsumed;
    consumed[1] = yConsumed;

    if (accepted) {
        onChildViewsChanged(EVENT_NESTED_SCROLL);
    }
}

上述方法中,CoordinatorLayout自身并没有消费距离,消费与否全由BehavioronNestedPreScroll方法决定,这是很容易想明白的,CoordinatorLayout是协调者布局,它自身没有消费事件的必要,全权交由Behavior处理符合自己的职责,不仅是onNestedPreScroll,下面全部可消费距离的方法都没有消费。

CoordinatorLayout#onNestedScroll

//处理大致和onNestedPreScroll一样,只分析其回调函数即可
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
                           int dxUnconsumed, int dyUnconsumed, @ViewCompat.NestedScrollType int type,
                           @NonNull int[] consumed) {
    ...

    for (int i = 0; i < childCount; i++) {
        ...
        //本篇文章出现的Behavior的第七个方法
 		viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
                                        dxUnconsumed, dyUnconsumed, type, mBehaviorConsumed);
        ...
    }

   	...
}

CoordinatorLayout#onNestedPreFling

public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
    boolean handled = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        ...
        //本篇文章出现的Behavior的第八个方法
        handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY);
        ...
    }
    return handled;
}

CoordinatorLayout#onNestedFling

public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
    boolean handled = false;

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
		...
        本篇文章出现的Behavior的第九个方法
        handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY,
                                              consumed);
		...
    }
	...
    return handled;
}

CoordinatorLayout#onStopNestedScroll

public void onStopNestedScroll(View target, int type) {
    //之前文章已经解析,不再赘述
    mNestedScrollingParentHelper.onStopNestedScroll(target, type);

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        ...
        //本篇文章出现的Behavior的第十个方法
        viewBehavior.onStopNestedScroll(this, view, target, type);
        ...

    }
    mNestedScrollingTarget = null;
}

接口中的方法和Behavior中方法的对应关系如下:

NestedScrollingParentBehavior
onStartNestedScroll()onStartNestedScroll()
onStopNestedScroll()onStopNestedScroll()
onNestedScroll()onNestedScroll()
onNestedPreScroll()onNestedPreScroll()
onNestedFling()onNestedFling()
onNestedPreFling()onNestedPreFling()
onNestedScrollAccepted()onNestedScrollAccepted()

上述的7个方法都是全权依赖于Behavior,属于委托实现

总结

绝大部分知识点都是嵌套滑动中的知识,我们只需要注意CoordinatorLayout是怎么回调Behavior即可,上面分析可知,每个方法中都有一个for循环,其回调的时机和是否指定Behavior存在直接关系,只要指定了Behavior且支持嵌套滑动,则Behavior的相关方法一定会触发,这也是和上个功能最大的区别,在依赖交互中只有存在依赖关系的View会触发回调。

子控件的测量和布局

测量和布局则是onMeasure()方法和onLayout()方法

测量onMeasure()

不对CoordinatorLayout具体的测量细节进行解析,我们关注的重点是onMeasure()如何做到协调子View

CoordinatorLayout#onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    prepareChildren();
    ensurePreDrawListener();
	...

    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        
 		...
        //
        final Behavior b = lp.getBehavior();
        //假设说Behavior的onMeasureChild返回true则代表子View测量事件交由Behavior处理,本篇文章出现的Behavior的第11个方法
        if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                childHeightMeasureSpec, 0)) {
            onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0);
        }
		...
    }

	...
    setMeasuredDimension(width, height);
}

总结:在方法的开始构建依赖,后面遍历依赖,执行依赖ViewBehavioronMeasureChild()方法,如果返回true则代表此次测量交由Behavior处理,如果false则调用默认的子View测量方法onMeasureChild()

布局onLayout()

实现与onMeasure()类似

CoordinatorLayout#onLayout

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);
		...
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior behavior = lp.getBehavior();
		//本篇文章出现的Behavior的第12个方法
        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
            onLayoutChild(child, layoutDirection);
        }
    }
}

总结

上述出现的两个Behavior方法的作用是拦截,如果Behavior实现且返回true,则不走默认实现

子控件事件拦截和响应

本篇文章不对传统事件分发进行讲解,若读者不熟悉传统事件分发,请看笔者的Android事件分发机制详解

CoordinatorLayout并没有实现dispatchTouchEvent()CoordinatorLayout作为协调者布职责就是协调,自身也不应该干预事件的分发,这是符合设计逻辑的。

CoordinatorLayout实现了onInterceptTouchEvent()onTouchEvent()

onInterceptTouchEvent()

CoordinatorLayout#onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent ev) {
    final int action = ev.getActionMasked();

    if (action == MotionEvent.ACTION_DOWN) {
        //重置上次的事件,为新的事件流做准备
        resetTouchBehaviors(true); //看下1
    }
	//是否拦截此次事件
    final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT); //看下2,拦截的重要方法

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors(true);
    }

    return intercepted;
}

如果是Down,Up,Cancel事件则会清空上次的事件,而真正的拦截是交给了**performIntercept()**方法

1.CoordinatorLayout#resetTouchBehaviors

private void resetTouchBehaviors(boolean notifyOnInterceptTouchEvent) {
    final int childCount = getChildCount();
    //遍历所有存在Behavior的View处理cancel事件,如果notifyOnInterceptTouchEvent为true则成功触发onInterceptTouchEvent,如果为false则触发onTouchEvent
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if (b != null) {
            final long now = SystemClock.uptimeMillis();
            final MotionEvent cancelEvent = MotionEvent.obtain(now, now,
                                                               MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            if (notifyOnInterceptTouchEvent) {
                b.onInterceptTouchEvent(this, child, cancelEvent); //Behavior的第13个方法
            } else {
                b.onTouchEvent(this, child, cancelEvent); //Behavior的第14个方法
            }
            cancelEvent.recycle();
        }
    }
	//清空所有View的滑动标志位
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        lp.resetTouchBehaviorTracking();//mDidBlockInteraction = false;此标志位在未来是有用的
    }
    //重置上次拦截事件的View,此View会在下2中赋值
    mBehaviorTouchView = null;
    mDisallowInterceptReset = false;
}

2.CoordinatorLayout#performIntercept

private boolean performIntercept(MotionEvent ev, final int type) {
    boolean intercepted = false;
    boolean newBlock = false;

    MotionEvent cancelEvent = null;

    final int action = ev.getActionMasked();
	//Material Design与普通View的最大不同就是Z轴的存在,上层的View和下层的View,到底谁应该来触发事件呢?,肯定是上层的View,因此在CoordinatorLayout中,要对子View进行高度排序,在遍历时从高到低遍历
    final List<View> topmostChildList = mTempList1;
    getTopSortedChildren(topmostChildList);

    //遍历子View
    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();
		//在非Down事件下,之前的View已经拦截或者不允许后续的View相应,则后续的View要执行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的onInterceptTouchEvent()和onTouchEvent()决定
        if (!intercepted && b != null) {
            switch (type) {
                case TYPE_ON_INTERCEPT:
                    intercepted = b.onInterceptTouchEvent(this, child, ev);
                    break;
                //在onTouchEvent()中会走此分支,主要是保护作用
                case TYPE_ON_TOUCH:
                    intercepted = b.onTouchEvent(this, child, ev);
                    break;
            }
            //拦截则保存下此View,mBehaviorTouchView在新的事件流或者此次事件流结束时会被在上述1中被清空
            if (intercepted) {
                mBehaviorTouchView = child;
            }
        }
		
        
        //后边这段代码不好理解
        //旧的是否拦截标志位,Down事件一定为false
        final boolean wasBlocking = lp.didBlockInteraction(); //返回mDidBlockInteraction的值
        //当前新的标志位的值,修改mDidBlockInteraction的值,并返回新的mDidBlockInteraction
        final boolean isBlocking = lp.isBlockingInteractionBelow(this, child); //看下3
        newBlock = isBlocking && !wasBlocking;
        if (isBlocking && !newBlock) {//只有当Behavior的blocksInteractionBelow()方法返回为true且wasBlocking为true时,if才会命中
            // 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;
        }
        //笔者感觉for循环开头的if和结尾处的if可能有问题,笔者说下自己的理解,可能笔者的理解也存在错误
        //首先要正确理解普通事件分发,什么情况下onInterceptTouchEvent方法会被调用,Down事件或者是其他事件流的Down被子View响应的情况下才会执行。mDidBlockInteraction标志位在down事件分发时被正确赋值,假设其他子View响应了Down事件,且能消费Move事件,例如文章开头的效果,则performIntercept()在滑动过程中会被不断被调用。for循环开头的if在不是down事件且newBlock || intercepted的时候命中,我们现在分析的是Move事件因此第一个条件满足,newBlock由Behavior的blocksInteractionBelow()方法决定,intercepted由Behavior的onInterceptTouchEvent()方法决定,我们进行排列组合一共有四种情况,(intercepted,newBlock)(假,假),(假,真),(真,真),(真,假)
        //(假,假)每个View都有可能有响应事件的权力,直到层级高的View拦截事件或者不希望层级低的View响应事件时
        //(假,真)newBlock为true只有在down事件时才有可能(读者可以试着自己分析),看for循环的最后几行代码,wasBlocking为flase且isBlocking为true的时候newBlock才为true,又因为此if只可能在不是down的情况下命中,因此此条件不成立。
		// (真,真)上一种情况newBlock为true不成立,此情况也不存在
        // (真,假)高层级View拦截事件,则其他View触发Cancel事件
        //再分析最后一个if,其作用是没有需要执行cancel事件的View了,直接break,笔者感觉效果不是这样的,分析如下if命中的条件是isBlocking为true,newBlock为false时,也就是说wasBlocking为true时,Down事件中由于wasBlocking一定为false,永远无法命中if,Move事件中当wasBlocking为true时,则isBlocking也必然为true,此时if命中,结束循环
        //综上,只要设置Behavior的blocksInteractionBelow()方法为true,则在move事件中,比当前View层级低的View永远无法响应事件,包括cancel事件,cancel事件只有当非Down且Behavior的blocksInteractionBelow()为false,Behavior的onInterceptTouchEvent()为true时才会触发。笔者的理解为Behavior的blocksInteractionBelow()方法不应该有拦截cancel事件的能力,但是恰恰此种代码设计让其有了这种能力。
       
    }
    topmostChildList.clear();

    return intercepted;
}

3.CoordinatorLayout#isBlockingInteractionBelow

boolean isBlockingInteractionBelow(CoordinatorLayout parent, View child) {
    if (mDidBlockInteraction) {
        return true;
    }
	//此表达式有些华丽花哨拆解一下
    //mDidBlockInteraction = (mDidBlockInteraction | (mBehavior != null)) ? mBehavior.blocksInteractionBelow(parent, child) : false;最终return mDidBlockInteraction;
    //在Behavior存在时,此值由Behavior的blocksInteractionBelow()方法决定,此方法的作用就是给不给下层的View事件,如果为true,层级低的View就不能相应事件
    return mDidBlockInteraction |= mBehavior != null
            ? mBehavior.blocksInteractionBelow(parent, child)//Behavior的第15个方法
            : false;
}

onTouchEvent

public boolean onTouchEvent(MotionEvent ev) {
    boolean handled = false;
    boolean cancelSuper = false;
    MotionEvent cancelEvent = null;

    final int action = ev.getActionMasked();
	//这里也存在一个问题,虽然说mBehaviorTouchView绝大是否不为null,但是如果为null,将会触发两次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);)//Behavior的第16个方法
        }
    }

    // Keep the super implementation correct
    if (mBehaviorTouchView == null) {
        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);
    }

    if (cancelEvent != null) {
        cancelEvent.recycle();
    }

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors(false);
    }

    return handled;
}

上述代码还是挺好理解,就是执行performIntercept中保存下来的ViewBehavioronTouchEvent

Behavior的方法总结

public static abstract class Behavior<V extends View> {

    public Behavior() {
    }

    public Behavior(Context context, AttributeSet attrs) {
    }

    //Behavior和LayoutParams关联上时会被调用
    public void onAttachedToLayoutParams关联上时会被调用(@NonNull CoordinatorLayout.LayoutParams params) {
    }

   	//Behavior和LayoutParams取消关联上时会被调用
    public void onDetachedFromLayoutParams() {
    }

    //子View拦截事件
    public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
            @NonNull MotionEvent ev) {
        return false;
    }

    //子View消费事件
    public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
            @NonNull MotionEvent ev) {
        return false;
    }

   //决定视图是否依赖
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child,
            @NonNull View dependency) {
        return false;
    }

    //依赖试图改变时回调
    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child,
            @NonNull View dependency) {
        return false;
    }

 	//移除依赖时回调
    public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull V child,
            @NonNull View dependency) {
    }

   	//测量视图时回调
    public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        return false;
    }

 	//布局视图时回调
    public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child,
            int layoutDirection) {
        return false;
    }


   
   	//NestedScrol则是嵌套滑动中的内容,不过多解释
    @Deprecated
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
            @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
            @ScrollAxis int axes) {
        return false;
    }

   
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
            @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
            @ScrollAxis int axes, @NestedScrollType int type) {
        if (type == ViewCompat.TYPE_TOUCH) {
            return onStartNestedScroll(coordinatorLayout, child, directTargetChild,
                    target, axes);
        }
        return false;
    }

  
    @Deprecated
    public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
            @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
            @ScrollAxis int axes) {
        // Do nothing
    }

   
    public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
            @NonNull V child, @NonNull View directTargetChild, @NonNull View target,
            @ScrollAxis int axes, @NestedScrollType int type) {
        if (type == ViewCompat.TYPE_TOUCH) {
            onNestedScrollAccepted(coordinatorLayout, child, directTargetChild,
                    target, axes);
        }
    }

   
    @Deprecated
    public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
            @NonNull V child, @NonNull View target) {
        // Do nothing
    }

   
    public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
            @NonNull V child, @NonNull View target, @NestedScrollType int type) {
        if (type == ViewCompat.TYPE_TOUCH) {
            onStopNestedScroll(coordinatorLayout, child, target);
        }
    }

 
    @Deprecated
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
            @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed) {
        // Do nothing
    }

    @Deprecated
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
            @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, @NestedScrollType int type) {
        if (type == ViewCompat.TYPE_TOUCH) {
            onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
                    dxUnconsumed, dyUnconsumed);
        }
    }

    
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
            @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, @NestedScrollType int type, @NonNull int[] consumed) {
        // In the case that this nested scrolling v3 version is not implemented, we call the v2
        // version in case the v2 version is. We Also consume all of the unconsumed scroll
        // distances.
        consumed[0] += dxUnconsumed;
        consumed[1] += dyUnconsumed;
        onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, type);
    }

    @Deprecated
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
            @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
        // Do nothing
    }

    
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
            @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
            @NestedScrollType int type) {
        if (type == ViewCompat.TYPE_TOUCH) {
            onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        }
    }

  
    public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
            @NonNull V child, @NonNull View target, float velocityX, float velocityY,
            boolean consumed) {
        return false;
    }

  
    public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
            @NonNull V child, @NonNull View target, float velocityX, float velocityY) {
        return false;
    }

  

	...
}

为什么需要Behavior?

Behavior是插件,热拔插,想用时set,不想使用时拔出即可,

将协调的工作交给Behavior,做到解耦的效果

总结

CoordinatorLayout来说还是比较复杂的,我们研究的重点就是其怎么协调,Behavior是怎么工作的,重点是Behavior的四个功能

笔者的理解可能有错,希望读者提出自己的见解,笔者加以改正。

原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下}

👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!}

⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!}

✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值