- CoordinatorLayout介绍、使用和原理
- 嵌套滑动机制
- 依赖管理机制
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并不难,由简到难手把手带你撸三款