• CoordinatorLayout介绍、使用和原理
  1. 嵌套滑动机制
  2. 依赖管理机制

CoordinatorLayout介绍

CoordinatorLayout is a super-powered FrameLayout.
CoordinatorLayout is intended for two primary use cases:
  1. As a top-level application decor or chrome layout
  2. As a container for a specific interaction with one or more 
child views

By specifying Behaviors for child views of a CoordinatorLayout 
you can provide many different interactions within a single 
parent and those views can also interact with one another.复制代码

CoordinatorLayout作为协调一个或多个子控件的根布局,子控件使用Behavior来和父控件或其他子控件进行交互。其中只能是直接子View,并不解析子控件的子控件。

CoordinatorLayout的使用核心就是Behavior,使用Behavior来执行交互。

Behavior介绍

CoordinatorLayout.Behavior<T>T是指这个Child,而不是dependency。Child是指这个CoordinatorLayout的子控件,dependency是指这个Child所依赖的View,即Child依赖于dependency的变化。

这是一个观察者模式的运用,Child向CoordinatorLayout注册一个回调,告知CoordinatorLayout在dependency发生变化时通知它,这样当dependency发生变化时,Child会得到这个通知。

其中Behavior属于子控件的属性,是CoordinatorLayout.Params的一个变量。

添加Behavior的几种方式

1. 布局中使用app:layout_behavior属性

<View
        android:id="@+id/child"
        android:layout_width="150dp"
        android:layout_height="150dp"
        app:layout_behavior="xxx"
        />复制代码

原理是CoordinatorLayout在generateLayoutParams中创建LayoutParams时,会调用parseBehavior方法,获取该子控件的Behavior。
此种方法必须要复写双参构造器。

try {
        Map<String, Constructor<CoordinatorLayout.Behavior>> constructors = sConstructors.get();
        if (constructors == null) {
            constructors = new HashMap<>();
            sConstructors.set(constructors);
        }
        Constructor<CoordinatorLayout.Behavior> c = constructors.get(fullName);
        if (c == null) {
            final Class<CoordinatorLayout.Behavior> clazz = (Class<CoordinatorLayout.Behavior>) Class.forName(fullName, true,
                                                                                                              context.getClassLoader());
           /*
             反射通过双参构造器创建对象,所以必须要复写双参构造器 
          */
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }复制代码

2. 添加注解

@CoordinatorLayout.DefaultBehavior(ABehavior.class)
public class AView extends View {
    //...
}复制代码

原理是CoordinatorLayout在onMearsure时调用prepareChildren方法时,遍历子控件解析LayoutParams时获取该子控件的Behavior。
此种方法必须要复写无参构造器。

LayoutParams getResolvedLayoutParams(View child) {
        final LayoutParams result = (LayoutParams) child.getLayoutParams();
        if (!result.mBehaviorResolved) {
            Class<?> childClass = child.getClass();
            DefaultBehavior defaultBehavior = null;
            // 遍历获取defaultBehavior注解值,包括其父类的,所以使用注解时具有继承性Behavior
            while (childClass != null &&(defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
                childClass = childClass.getSuperclass();
            }
            if (defaultBehavior != null) {
                //通过无惨构造器反射创建对象,所以需要复写午餐构造器
                try {
                    result.setBehavior(defaultBehavior.value().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;
    }复制代码

3. CoordinatorLayoutParams.setBehavior

在SnackBar中,判断LayoutParams instance CoordinatorLayout.Params,如果是,new Bahavior()并且通过param.setBehavoir设置。

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){}

// 嵌套滚动的方法,同NestedScrollingParent中方法
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes){}
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes){}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target){}
public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed){}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed){}
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,float velocityX, float velocityY, boolean consumed){}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,float velocityX, float velocityY){}

// 一些供子View控制的方法,measure和touch事件进行拦截处理
public boolean onMeasureChild(CoordinatorLayout parent, V child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed){}
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection){}
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev){}
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev){}

//...其他方法

// 其中的各种回调都是从CoordinatorLayout中进行分发,比较细节,就不赘述了复制代码

关于使用请看:CoordinatorLayout自定义Behavior的运用

接下来从CoordinatorLayout加载Child开始理解。

CoordinatorLayout依赖管理机制

这块来理解CoordinatorLayout是怎么管理各个子View的。子View的依赖关系,它们是如何互相依赖的。

// 依赖关系排序后的图,根据子View的依赖关系来measure、layout、按照顺序触发回调等
private final List<View> mDependencySortedChildren = new ArrayList<>(); 
// 有向无环图,使用邻接表表示该图
private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();

/*
  onMeasure时调用该方法,初始化该图
*/
private void prepareChildren() {
    mDependencySortedChildren.clear();
    mChildDag.clear();

    // 嵌套循环寻找依赖,构建图和边。
    for (int i = 0, count = getChildCount(); i < count; i++) {
        final View view = getChildAt(i);

        final CoordinatorLayout.LayoutParams lp = getResolvedLayoutParams(view);
        lp.findAnchorView(this, view);

        mChildDag.addNode(view);

        // Now iterate again over the other children, adding any dependencies to the graph
        for (int j = 0; j < count; j++) {
            if (j == i) {// 不能依赖自己
                continue;
            }
            final View other = getChildAt(j);
            final CoordinatorLayout.LayoutParams otherLp = getResolvedLayoutParams(other);
            if (otherLp.dependsOn(this, other, view)) {
                if (!mChildDag.contains(other)) {
                    // Make sure that the other node is added
                    mChildDag.addNode(other);//添加结点
                }
                // Now add the dependency to the graph
                mChildDag.addEdge(view, other);//添加弧
            }
        }
    }

    /*
       获取拓扑排序后的List, 根据依赖关系调用Child的Behavior进行onMeasure和onLayout。
    */
    // 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);
}

boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency == mAnchorDirectChild //layout_archor
            || shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))
            || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}复制代码

CoordinatorLayout里面可以放置很多的子View,并且任何一个子View可以依赖别的子View,所以源码中使用了有向图来表示这种关系。
至于是无环的,从直觉上来看,如果A依赖B,B也依赖A,则无法判断谁先绘制等一系列操作;从代码上看,在DirectedAcyclicGraph中,使用DFS进行拓扑排序时,并检查了是否有环,如果发现有互相依赖的节点,则会抛出RuntimeException。
所以必须要求子View间只能单向依赖。

private void dfs(final T node, final ArrayList<T> result, final HashSet<T> tmpMarked) {
  // ...
  if (tmpMarked.contains(node)) {
    throw new RuntimeException("This graph contains cyclic dependencies");
  }
  // ...
}复制代码

嵌套滑动机制

在触发一系列滚动方法时,使用到了嵌套滑动机制。这块主要描述该机制。

/*
This interface should be implemented by ViewGroup 
subclasses that wish to support scrolling operations delegated 
by a nested child view.

Classes implementing this interface should create a final 
instance of a NestedScrollingParentHelper as a field and 
delegate any View or ViewGroup methods to the 
NestedScrollingParentHelper methods of the same signature.
*/
NestedScrollingParent
NestedScrollingParentHelper

/*
This interface should be implemented by View subclasses that 
wish to support dispatching nested scrolling operations to a 
cooperating parent ViewGroup.

Classes implementing this interface should create a final 
instance of a NestedScrollingChildHelper as a field and 
delegate any View methods to the NestedScrollingChildHelper 
methods of the same signature.
*/
NestedScrollingChild
NestedScrollingChildHelper复制代码

NestedScrollingChild将所有的接口方法delegate到NestedScrollingChildHelper中,并且在onIntercepter、onTouchEvent中调用这些方法,将事件通过接口方法dispatch给NestedScrollingParent,在NestedScrollingParent中对事件进行处理,完成嵌套滚动。
从描述可以看出,子View接收事件,通过view.getParent()方法获取父控件,在此将事件分发到父View,让父View有机会去消耗子View的事件,这种思想值得学习。

在CoordinatorLayout中,所有的Behavior都有机会拦截事件,并接收从NestedScrollingParent中的所有嵌套滚动方法。




参考:

  • 介绍
    CoordinatorLayout的使用如此简单彻底搞懂CoordinatorLayoutCoordinatorLayout 子 View 之间的依赖管理机制 —— 有向无环图【图论】有向无环图的拓扑排序
  • 自定义Behavior示例
    CoordinatorLayout自定义Behavior的运用
  • 嵌套滑动
    Android NestedScrolling机制完全解析 带你玩转嵌套滑动NestedScrolling事件机制源码解析
  • 例子:
    CoordinatorBehaviorExample一个神奇的控件——Android CoordinatorLayout与Behavior使用指南CoordinatorLayout 自定义Behavior并不难,由简到难手把手带你撸三款