Behavior 原理简析

前言

2014 年 Google I/O 推出了 Material Design 的设计规范,Android 也顺应推出了相关的 Support Design 支持库。库中包含 AppBarLayout,FloatingActionButton 等符合设计规范的控件,但其中最重要的是一个 CoordinatorLayout 的容器,最大的作用就是为子 View 间交互提供便利,我们可以在各大app上(知乎,uc等看到它的身影)

引入

gradle配置

1
2
3
4
5
dependencies {

......
compile 'com.android.support:design:23.4.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

CoordinatorLayout 可以作为一个布局文件的根部局,方便子 View 间的相互作用。CoordinatorLayout 的继承结构如下:

1
2
public class CoordinatorLayout 
extends ViewGroup implements NestedScrollingParent

CoordinatorLayout 只实现了 NestedScrollingParent 接口,而没有 NestedScrollingChild 接口,那么

  • 这个布局控件不适用于作为一个子 View 做嵌套布局
  • 5.0 以下版本的 ScrollView 和 ListView 因为没有实现 NestedScrollingChild 接口而无法在 CoordinatorLayout 布局上运用,推荐使用 RecyclerView 和 NestedScrollView

而 CoordinatorLayout 实现子 View 间交互的最大功臣就是 Behavior —— 一个 CoordinatorLayout 的内部类

Behavior简介

CoordinatorLayout.Behavior 官方介绍(需要科学上网)

该类比较简单,只是定义一些方法,需要时我们可以自定义 behavior 类继承该类,通过复写相关方法来完成子 View 的代理

  • View 的绘制
    • onMeasureChild
    • onLayoutChild
  • View 的事件分发
    • onInterceptTouchEvent
    • onTouchEvent
  • child 的嵌套滑动响应
    • onStartNestedScroll
    • onNestedPreScroll
    • onNestedScroll
    • onStopNestedScroll
  • child 间的依赖
    • layoutDependsOn
    • onDependentViewChanged

在 Support Design 库中有以下几个类 AppBarLayout.Behavior, AppBarLayout.ScrollingViewBehavior, BottomSheetBehavior, FloatingActionButton.Behavior, SwipeDismissBehavior 是直接继承 Behavior 的,至于内部添加哪些方法的具体逻辑要看具体的 View 使用场景而定

CoordinatorLayout 和 Behavior 配合

那他们之间是如何协同工作的呢?
在 CoordinatorLayout 的自定义 LayoutParams 中有 Behavior 变量对象,在它的构造函数中有获取 behavior 实例的相关代码

1
2
3
4
5
6
7
8
9
10
11
LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);

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

a.recycle();
}

parseBehavior 方法通过布局文件中设置的相关字符串解析得到实例,值得一提的是只有 CoordinatorLayout 的子 View 设置了 behavior 才有用,CoordinatorLayout 就可以在合适的时机通过这些设置在 lp 中的 behavior 来管理对应的子 View
比如说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
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();

if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}

只有当子 View 对应的 behavior 的 onLayoutChild 方法返回 false 的时候才会调用父 View 的方法。由此可见,behavior 的优先级是高于父类方法的,Behaivor 的触摸分发,绘制类似,具体代码不在此展示,有兴趣可以直接看源码

  • 滑动处理:CoordinatorLayout 继承了 NestedScrollingParent,而 behavior 代理实现 NestedScrollingChild 的相关方法实现嵌套滑动,当子View在处理滑动事件之前,先告诉自己的父 View 是否需要先处理这次滑动事件,父 View 处理完之后,告诉子 View 它处理的多少滑动距离,剩下的还是交给子 View 自己来处理,当然具体逻辑也是比较复杂的
  • 子 View 间的交互代理:主要用于监听同级别子 view 的大小、位置、显示状态的改变,通过一下几个方法实现的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 确定所提供的子视图是否有另一个特定的同级视图作为布局从属
*/
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}

/**
* 用于响应从属布局的变化
*/
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}

/**
* 用于响应从属布局的移除
*/
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
}

在自定义的 behavior 中,layoutDependsOn 方法可以定义索要依赖的同级控件类型,比如我们需要依赖 Button,那么就需要return dependency instanceof Button;而一般而言如何响应从属布局的变化,则需要我们在 onDependentViewChanged 中书写具体的逻辑。视线转回 CoordinatorLayout 中, onChildViewsChanged 方法中会有这 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
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final View child = mDependencySortedChildren.get(i);

......

for (int i = 0; i < childCount; i++) {

......

// 更新有通过 behavior 依赖关系的 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();

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:
// 如果是 view 移除事件,需要调用 onDependentViewRemoved 方法
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// 否则就需要调用 onDependentViewChanged 方法
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}

if (type == EVENT_NESTED_SCROLL) {
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
}

值得注意的是该方法的调用是在 view 进行绘制之前

1
2
3
4
5
6
7
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}

而在 onNestedScroll,onNestedPreScroll,onNestedFling 方法中也会调用到 onChildViewsChanged(EVENT_NESTED_SCROLL) 方法
,所以在该方法中也会进行两个状态冲突的处理
值得注意的是两个 view 之间并不能相互依赖 mDependencySortedChildren 是类中确定依赖关系顺序的变量,在 prepareChildren 方法中,系统会按照依赖关系对所有的子 View 进行排序。因为使用的是有向无环图,所以不会产生相互依赖的情况产生,同时这个变量会影响测量和绘制的顺序。’

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
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();

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

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

mChildDag.addNode(view);

// 遍历其他的除自己以外的同级 view,判断是否有依赖关系,有的话会将两个 view 在无环有向图中添加指向
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
final LayoutParams otherLp = getResolvedLayoutParams(other);
if (otherLp.dependsOn(this, other, view)) {
if (!mChildDag.contains(other)) {
// 确保 view 已经被添加进图中成为一个节点
mChildDag.addNode(other);
}
// 添加图中依赖关系
mChildDag.addEdge(view, other);
}
}
}

// 最后,把生成的图添加到我们的mDependencySortedChildren中,感兴趣的可以去查找DAG的dfs方法,然后进行一次翻转使view的顺序为后者依赖前者
Collections.reverse(mDependencySortedChildren);
}

这个方法会在 onMeasure 中调用,所以在一开始 CoordinatorLayout 就确定由依赖关系确定子 view 的调用关系

behavior 的简单使用

behavior 定义在 CoordinatorLayout 子 view 的布局文件中,用app:layout_behavior命名空间修饰,传入定义好的 behavior 全路径名即可,当然你可也可以自定义 Behavior