CoordinatorLayout之Behavior的使用

       CoodinatorLayout并不知道FloatingActionButton和AppBarLayout的工作原理,在CoordinatorLayout的基本使用提到CoodinatorLayout实现了NestedScrollingParent,通过一个实现了NestedScrollingChild的scrolling view,就可以轻松的实现滑动事件的处理与View之间的交互。这其中充当中间桥梁的就是CoordinatorLayout.Behavior,比如FloatingActionButton,查看源码发现它的类注解是这样的:

1
2
3
4
@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
public class FloatingActionButton extends VisibilityAwareImageButton {
// ...
}

      FloatingActionButton.Behavior 的主要作用就是防止被Snackbar盖住。

      自定义View既可以通过注解指定Behavior,也可以通过在布局XML申明:

1
app:layout_behavior="具体Behavior的类路径"

      Behavior主要功能有三块:

  1. onLayoutChild和onMeasureChild
    跟布局有关的,这部分控制界面位置和大小

  2. layoutDependsOn和onDependentViewChanged
    跟别的子view的位置互动 layoutDependsOn和onDependentViewChanged

  3. onXXXSroll和onXXXFling
    跟嵌套滑动有关 onStartNestedScroll onNestedPreScroll onNestedScroll onStopNestedScroll onNestedPreFling onNestedFling

onMeasureChild和onLayoutChild

onMeasureChild

1
2
3
4
5
public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
return false;
}

       这个方法用来计算child的宽高(getMeasuredWidth和getMeasuredHeight),这部分和view的绘制方法和作用都是一样的。

1
2
int newHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(300, View.MeasureSpec.EXACTLY);
int newWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec(500, View.MeasureSpec.EXACTLY);

       如上,我们重新设定parent的宽高是500*300,然后用新的参数测量child:

1
parent.onMeasureChild(child, newWidthMeasureSpec, widthUsed, newHeightMeasureSpec, heightUsed);

       输出日志如下:

1
com.wy521angel.coordinatorlayouttest I/MeasureBehavior: onMeasureChild==========w=500   h=300

       布局如下:

1
2
3
4
5
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_light"
app:layout_behavior=".MeasureBehavior"/>

       就像下图的蓝色的方块,即使在xml文件设定的宽高是match_parent,也是显示成代码里面设置的500*300。

Behavior的测量与布局

onLayoutChild

1
2
3
4
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child,
int layoutDirection) {
return false;
}

       这个方法是用来放置child的位置的:

1
2
3
4
5
@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
child.layout(0, 500, child.getMeasuredWidth(), 500 + child.getMeasuredHeight());
return true;
}

       像上面设置之后,效果是Behavior的测量与布局图中的红色方块,往下移动500。

       另外,onInterceptTouchEvent和onTouchEvent这两个功能和view的onInterceptTouchEvent和onTouchEvent是一样的,在这两个方法里面进行一些事件的分发,返回true就可以代替view自己的两个方法。这个部分的内容其实和View的绘制和事件分发是一样的。

layoutDependsOn和onDependentViewChanged

       这是Behavior比较核心的一个功能。

       Behavior可以让一个child根据另外的一个child的位移,来改变自己的状态,无论是位置还是透明度等一些属性。

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

       这个方法主要是决定当前child要根据哪个child(即dependency)来改变自己,可以用dependency的id、tag、类型等一些办法来判定是否是dependency。

       类似ScrollingViewBehavior就是根据view的类型来判断的,如下

1
2
3
4
5
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
// We depend on any AppBarLayouts
return dependency instanceof AppBarLayout;
}

       当确定好dependency,就可以用下面方法根据dependency位置变化来改变自己的状态了:

1
2
3
4
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull View dependency) {
return false;
}

       代码如下:

1
2
3
4
5
6
7
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
int dependBottom = dependency.getBottom();
child.setY(dependBottom + 50);
child.setX(dependency.getLeft());
return true;
}

       红色方块会跟着绿色方块移动,如下图:

onXXXXXXXScroll

       这个部分也是Behavior比较核心的一个功能,关于这部分的原理解释起来会经常涉及到嵌套滑动,详见嵌套滑动与冲突解决

       onXXXXXXXFling部分跟Scroll流程是类似的,此处省略。

       CoordinatorLayout实现了NestedScrollingParent就能接收实现了NestedScrollingChild接口的child的滑动事件,它接收到滑动事件后就会把事件发送给每个愿意接收这个事件的child,至于怎么确定是否愿意,是在这个方法onStartNestedScroll:

1
2
3
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, int axes) {
return false;
}

       只要返回true就表示这个child愿意接收这个滑动事件。如果愿意接收这个滑动事件,就会在接下来的这两个方法onNestedPreScroll、onNestedScroll接收到事件。

       这里先简单介绍一下这两个方法的流程,当一个NestedScrollingChild的childA发生滑动的时候,会先询问有没有childB愿意接收这个滑动事件,愿意的话在onStartNestedScroll返回true,然后把这个事件会发给childB的onNestedPreScroll。

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

       在onNestedPreScroll里面,childB有可能会消费这个滑动事件,也有可能一点都不消费,或者消费部分,这个都是通过consumed来传回的。

       当childB的onNestedPreScroll结束后,childA根据consumed的值,判断是否还有剩下滑动距离没消费完。如果还有的话,childA就开始自己的滑动,当然childA依然还是有可能消费不完这次的滑动事件,比如滑到底部了,childA会再次把事件发给childB的onNestedScroll。

1
2
3
4
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
}

       如果childB还没消费完这个事件,会再返回到childA,自生自灭。

       看如下效果图:

       有个NestedScrollView和一个方块,运行规则是这个方块会根据NestedScrollView的反方向移动,当触顶或者触底的时候,才会轮到NestedScrollView自己滚动。consumed控制着方块的消费情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
Log.d(TAG, "onNestedPreScroll");
if (child.getY() + dy < 0) {
//方块到达顶部,滑动距离消费不完
ViewCompat.offsetTopAndBottom(child, (int) (0 - child.getY()));
consumed[1] = 0 - (int) child.getY();
} else if (child.getY() + dy > coordinatorLayout.getHeight() - child.getHeight()) {
//方块到达底部,滑动距离消费不完
ViewCompat.offsetTopAndBottom(child, (int) (coordinatorLayout.getHeight() - child.getHeight() - child.getY()));
consumed[1] = coordinatorLayout.getHeight() - child.getHeight() - (int) child.getY();
} else {
//方块消费完全部事件
ViewCompat.offsetTopAndBottom(child, dy);
consumed[1] = dy;
}
}

       Behavior的更多使用请看CoordinatorLayout之Behavior的相关实例,代码详见CoordinatorLayoutTest

参考资料:
zhuhf CoordinatorLayout 完全解析
Xugter CoordinatorLayout系列(二):Behavior的使用

Fork me on GitHub