透过源码学习CoordinatorLayout与Behavior

CoordinatorLayout(协调布局)是将嵌套滑动的泛用性发挥到极致的控件,只需要通过简单配置它的子View的Behavior(行为)就可以实现复杂而炫丽的效果。

Behavior

Behavior是CoordinatorLayout的内部静态类,通过继承Behavior可以实现自定义的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public static abstract class Behavior<V extends View> {
public Behavior() {
}

public Behavior(Context context, AttributeSet attrs) {
}

...

public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
return false;
}

public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent) {
return false;
}

public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency) {
return false;
}

/**
* @return true 这个Behavior改变了child view的大小或位置; 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) {
}

/**
* @return true 由此Behavior测量此View; false 父级CoordinatorLayout测量
*/
public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
return false;
}

/**
* @return true 由此Behavior布局此View; false 父级CoordinatorLayout布局
*/
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection) {
return false;
}

/**
* @return true 此Behavior接收嵌套滑动事件
*/
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;
}

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

public void onStopNestedScroll(@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, @NonNull int[] consumed) {
consumed[0] += dxUnconsumed;
consumed[1] += dyUnconsumed;
onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type)
}

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

以上源码摘录了Behavior常用的方法,从简单到复杂的需求皆可实现。简单需求可以搭配layoutDependsOnonDependentViewChanged,复杂需求可自行定制嵌套滑动的细节。

Behavior的绑定方式

通过布局属性绑定

即在布局文件中通过app:layout_behavior添加,看看源码是怎么解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}

final String fullName;
if (name.startsWith(".")) {
//相对于app的包,在前面添加app包名
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
//完整包名
fullName = name;
} else {
//WIDGET_PACKAGE_NAME = pkg != null ? pkg.getName() : null
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME) ? (WIDGET_PACKAGE_NAME + '.' + name) : name;
}

try {
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor<Behavior> c = constructors.get(fullName);
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);
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}

可以看到,在布局文件中通过填写的全名或者半名自动补全查找,然后通过反射实例化对应的Behavior类。接下来再来看看,这个方法在哪里调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static class LayoutParams extends MarginLayoutParams {
...
LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
...
mBehaviorResolved = a.hasValue(R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
a.recycle();

if (mBehavior != null) {
//解析完成并绑定成功后,调用回调方法通知
mBehavior.onAttachedToLayoutParams(this);
}
}
}

可以看到,Behavior是在LayoutParams初始化时解析并绑定的。

绑定默认行为

旧版本的默认行为是在自定义的类上添加注解**@DefaultBehavior(),而新版本则要求自定义类实现AttachedBehavior接口,并通过getBehavior()**方法返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
LayoutParams getResolvedLayoutParams(View child) {
final LayoutParams result = (LayoutParams) child.getLayoutParams();
if (!result.mBehaviorResolved) {
//就是这里,看到了吧
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 class的方式
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与行为交互

CoordinatorLayout的本质还是通过Behavior将嵌套滑动“发散”出去,“分给”一个或多个子View同步享用。要知道它是怎么做到的,只要看看它其中的与嵌套滑动相关的方法即可,例如onStartNestedScroll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
@SuppressWarnings("unchecked")
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);
if (view.getVisibility() == View.GONE) {
//如果它消失了,就不分发消息
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
//它会尝试从child的LayoutParams中获取Behavior
//并询问Behavior是否接收嵌套滑动事件
//如果接收,后续的事件就会发送给对应的Behavior
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}

onNestedScroll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Override
@SuppressWarnings("unchecked")
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed) {
final int childCount = getChildCount();
boolean accepted = false;
int xConsumed = 0;
int yConsumed = 0;

for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
continue;
}

final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted(type)) {
continue;
}

final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mBehaviorConsumed[0] = 0;
mBehaviorConsumed[1] = 0;

viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, mBehaviorConsumed);

xConsumed = dxUnconsumed > 0 ? Math.max(xConsumed, mBehaviorConsumed[0]) : Math.min(xConsumed, mBehaviorConsumed[0]);
yConsumed = dyUnconsumed > 0 ? Math.max(yConsumed, mBehaviorConsumed[1]) : Math.min(yConsumed, mBehaviorConsumed[1]);

accepted = true;
}
}

consumed[0] += xConsumed;
consumed[1] += yConsumed;

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

onNestedScroll方法中可以明显的看到,如果有其中任意一个Child的Behavior接收并消费了事件,那么就会在方法结束位置调用onChildViewChanged方法,onChildViewChanged顾名思义——有View发生了改变,很容易就可以联想到一定会触发某个Behavior的onDependentViewChanged方法(如果有View依赖这个消费事件的View的情况下)。

为了印证这种想法,就需要查看onChildViewChanged

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@SuppressWarnings("unchecked")
final void onChildViewChanged(@DispatchChangeEvent final int type) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
final Rect inset = acquireTempRect();
final Rect drawRect = acquireTempRect();
final Rect lastDrawRect = acquireTempRect();

for (int i = 0; i < childCout; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
continue;
}

//...省略一部分关于重定位的代码
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();

if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if(type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
checkLp.resetChangedAfterNestedScroll();
continue;
}

final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
b.onDependentViewRemoved(this, childChild, child);
handled = true;
break;
default:
//果然如此,这就是我们要找的
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}

if (type == EVENT_NESTED_SCROLL) {
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
releaseTempRect(inset);
releaseTempRect(drawRect);
releaseTempRect(lastDrawRect);
}

果不其然,在倒数几行的位置找到了handled = b.onDependentViewChanged(this, checkChild, child),整个流程就已经连起来了。

CoordinatorLayout

还记得刚才说Behavior绑定的时候找到的getResolvedLayoutParams吗,那么它是在哪里调用的呢,继续找,找到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();//这是一个有向无环图(DirectedAcyclicGraph<View>)
//也就是树

for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);

//解析并绑定Behavior
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);

mChildDag.addNode(view);

for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
if (lp.dependsOn(this, view, other)) {
if (!mChildDag.contains(other)) {
mChildDag.addNode(other);
}
mChildDag.addEdge(other, view);
}
}
}

//将通过深度优先搜索得到的排序过后的结果添加到mDependencySortedChildren中
mDependencySortedChildren.addAll(mChildDag.getSortedList());
Collections.reverse(mDependencySortedChildren);
}

然后,在onMeasure方法中调用,准备好所有的子节点,然后再开始测量,接着再进行布局。对于所有节点,都会尝试优先查找节点的LayoutParams是否包含Behavior,如果包含,再询问Behavior是否接管测量或者布局(即behavior.onMeasureChildbehavior.onLayoutChild)。