CoordinatorLayout.Behavior使用指南
1. Behavior 介绍
Behavior是CoordinatorLayout的子类,用于处理各个子view之间的行为(
这就意味着需要联动的View得用CoordinatorLayout包起来 ),例如:手指向下滑动的时候是View A下移,还是View
B下移,亦或者给View C来一个旋转?一个例子就是Bilibili客户端视频播放页的滑动效果,如图1所示:
图1. Bilibili客户端视频页滑动效果
2. 如何设置Behavior
设置Behavior有两种方式:XMl布局设置和Java(Kotlin)代码设置
2.1 XML布局设置
首先将CoordinatorLayout设置为父布局,对需要加Behavior控件设置app:layout_behavior"
属性。
<androidx.core.widget.NestedScrollView
...
app:layout_behavior= "com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
2.2 Java/Kotlin代码设置
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams)scrollView.getLayoutParams();
params.setBehavior(new MyBehavoir());
scrollView.setLayoutParams(params);
3.
AppBarLayout内部有一个默认实现的Behavior,经常和Toolbar、Recycler等结合使用,布局如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:fitsSystemWindows="true">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll"
app:layout_scrollInterpolator="@android:anim/decelerate_interpolator"
app:title="标题栏" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior= "com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
图2. AppBarLayout效果
AppBarLayout默认的Behavior是AppBarLayout$ScrollingViewBehavior
,可以设置layout_scrollFlags和layout_scrollInterpolator等参数,分别表示动画风格和动画差值器。正如上面的实例代码,我们对Toolbar设置了如下的属性,使toolbar可以跟随列表滚到而显示和隐藏。
<androidx.appcompat.widget.Toolbar
...
app:layout_scrollFlags="scroll"
app:layout_scrollInterpolator="@android:anim/decelerate_interpolator"
... />
3.1 layout_scrollFlags
layout_scrollFlags是ScrollingViewBehavior支持的一个属性,用来设置滚动的风格,包括移入、移出效果,一共有七个,分别是noScroll、scroll、exitUntilCollapsed、enterAlways、enterAlwaysCollapsed、snap和snapMargins,其效果如表1、表2所示。
noScroll | scroll | exitUntilCollapsed | enterAlways |
---|---|---|---|
表1. layout_scrollFlags的演示效果1 |
enterAlwaysCollapsed | snap | snapMargins |
---|---|---|
表2. layout_scrollFlags的演示效果2 |
关于这个七个标记的具体说明如表3所示。
标志 | 说明 |
---|---|
noScroll | 禁用视图上的滚动。此标志不应与其他任何标志相结合。 |
scroll | 这个标志会让view直接跟随滚动事件。如果其他标记需要在滚动的情况下使用,则必须结合scroll 一起使用,否则设置无效。 |
exitUntilCollapsed | 手指上滑会将view折叠,折叠的高度为minHeight的值,下拉再慢慢展开,高度为height的值。 |
enterAlways | |
这个标志和单纯的scroll 很相似,区别在与enterAlways 会在你下拉的时候就显示view,上滑的时候就隐藏view,而scroll 则必须要把列表滚到顶部之后才会显示、隐藏view。 |
|
enterAlwaysCollapsed | |
需要与enterAlways 搭配使用,效果是下拉时先显示折叠时的高度,继续下拉,列表到顶则会讲view展开。 |
|
snap | |
与单纯的scroll 效果很相似,区别在于snap 有吸附效果,当view的位置很靠近显示或者隐藏时,会使用动画自动显示或者隐藏,就像是有磁铁吸附的感觉。 |
|
snapMargins | 需要与snap 搭配使用,吸附位置为view的top和bottom margin值。 |
表3. layout_scrollFlags的参数说明 |
4 自定义Behavior
4.1 核心方法介绍
在正式自定义一个Behavior之前,先介绍一下Behavior的几个比较重要的方法:
方法 | 说明 |
---|---|
boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child, | |
@NonNull View dependency) | 判断child 是否依赖dependency |
boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V | |
child, @NonNull View dependency) | 当dependency 发生了变化的时候(位置、旋转角度等)会被调用 |
void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull V | |
child, @NonNull View dependency) | 当dependency 被移除之后会调用此方法。 |
boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, | |
int layoutDirection) | 在这里可以给child 进行初始位置的布局。返回ture,表示我们自定义的布局,否则使用系统默认的布局。 |
boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, | |
@NonNull V child, @NonNull View directTargetChild, @NonNull View target, | |
@ScrollAxis int axes, @NestedScrollType int type) | |
一个滚动事件等开始会触发此回调,可以在这里返回是否需要消耗本次滚动事件 | |
void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull | |
V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, | |
@NestedScrollType int type) | |
如果onStartNestedScroll 返回了true,则在滚动中会回调此方法,可以在这里做view做位置等等变化 |
|
void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull | |
V child, @NonNull View target, @NestedScrollType int type) | |
一个滚动事件等结束会触发此回调,可以在这里给child 添加回弹动画。 |
|
表4. Behavior的几个主要方法 |
4.2 基本流程
自定义Behavior的流程分为3步:
- 判断依赖关系,即谁依赖谁
- 当被依赖的view发生了变化时对依赖view进行变化
- 处理嵌套滚动事件
4.2.1 判断依赖关系,即谁依赖谁
假设A依赖B,则Behavior应该设置在A上面,然后在layoutDependsOn
方法里面,判断如果dependency
为B则返回true。
4.2.2 当被依赖的view发生了变化时对依赖view进行变化
dependency
的位置、方向等变化之后会调用onDependentViewChanged
方法,在这个方法里面,可以对view进行跟随变化,比如将view对bottom设置为dependency
top位置,则view会始终在dependency
的上面,高度跟随dependency
动态变化。如果需要在dependency
被移除的时候对view进行变化,则重写onDependentViewRemoved
即可。
4.2.3 处理嵌套滚动事件
处理一个滚动事件的流程是:
判断滑动方向是否是自己需要的
处理滑动值
滚动事件结束是否需要回弹动画
与此对应的三个方法是:
- boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type)
其中axes
表示的是本次滚动事件的方向、type
表示手指触摸或者是惯性滚动。
// 判断滚动是否是竖直方向
boolean result = (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
- void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type)
滚动事件里面的每一次滑动会调用此方法,在这里我们可以对滑动进行拦截处理。consumed
是一个数组,表示Behavior消耗了的X、Y方向的滑动值,消耗的滑动值就不会被传递为子view了。
- void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, @NestedScrollType int type)
滚动事件结束的时候会调用此方法,可以在这里判断view是否需要回弹到默认位置。
需要注意的地方是:通过动画移动view到默认位置也是会触发onNestedPreScroll
方法的,可以结合type
判断是否是手指触摸导致的。
5 实例
图3. 实例效果图
从图中可以知道,顶部的图片会跟随列表的滚动,列表下移的时候图片会先下移,超过默认高度之后会有一个放大的效果。也就是说背景图的高度和大小是依赖列表的位置的。所以,我们可以将Behavior设置在背景图上面,让其依赖列表。布局如图3所示。
图4. 布局文件
接着开始自定义MyBehavoir
。
重写layoutDependsOn
让图片依赖背景列表。
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
return dependency instanceof NestedScrollView;
}
当列表的位置变化的时候需要对图片的位置进行变化,使图片的底部始终跟随列表的顶部。
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
int top = dependency.getTop();
int bottom = child.getBottom();
ViewCompat.offsetTopAndBottom(child, top - bottom);
return true;
}
因为这个列表是竖直滚动的,所以我们只需要监听竖直方向的滚动。
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
让列表监听滚动事件,跟随手指滑动。
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
ViewCompat.offsetTopAndBottom(target, -dy);
consumed[1] = dy;
}
看看效果。
图5. 滑动效果演示
现在已经实现了图片底部贴着列表顶部,并且图片滚跟随列表移动。但是这里有一个问题,就是当图片不可见的时候滚动的应该是列表内部元素,而不是对列表进行位移。我们可以通过canScrollVertically
判断列表是否滚动到了顶部,如果列表可以向下滑动,则说明列表没有到顶部。
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
if (dy < 0) { //手指下滑
//列表是否可以继续下滑,如果不可以,则说明列表到顶了
boolean canScrollDown = target.canScrollVertically(-1);
if (!canScrollDown) {//列表到顶了,此时需要移动列表位置
ViewCompat.offsetTopAndBottom(target, -dy);
consumed[1] = dy; //滑动的距离被Behavior消耗了
}
} else if (dy > 0) { //手指上滑
//如果列表的位置已经在顶部了,则滑动内部元素,否则移动列表的位置
if (target.getTop() > 0) {
int maxDy = Math.min(target.getTop(), dy);
ViewCompat.offsetTopAndBottom(target, -maxDy);
consumed[1] = maxDy;
}
}
}
现在列表和图片都可以滚动了。
图6. 支持列表内部滚动
但是还是有点问题,列表会无限制的下移,我们可以限制列表下移为高度不超过图片的高度。
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
if (dy < 0) { //手指下滑
//列表是否可以继续下滑,如果不可以,则说明列表到顶了
boolean canScrollDown = target.canScrollVertically(-1);
if (!canScrollDown) {//列表到顶了,此时需要移动列表位置
//列表下移的位置不得超过 图片的高度
int maxDy = Math.max(dy, target.getTop() - child.getHeight());
ViewCompat.offsetTopAndBottom(target, -maxDy);
//滑动的距离被Behavior消耗了
consumed[1] = maxDy;
}
} else if (dy > 0) { //手指上滑
//如果列表的位置已经在顶部了,则滑动内部元素,否则移动列表的位置
if (target.getTop() > 0) {
int maxDy = Math.min(target.getTop(), dy);
ViewCompat.offsetTopAndBottom(target, -maxDy);
consumed[1] = maxDy;
}
}
}
效果如下:
图7. 限制列表下移的最大位置
现在就剩下最后一个问题了,那就是刚进来的时候图片不可见,我们希望布局的初始位置是图片显示默认高度,列表在图片的底部,所以我们可以重写onLayoutChild
对列表进行布局。
@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull View child, int layoutDirection) {
parent.onLayoutChild(child, layoutDirection);
//获取child以来的控件列表
List<View> dependencies = parent.getDependencies(child);
if (dependencies.size() > 0) {
//因为这个我们只依赖了一个列表,所以直接取第0个元素
View dependency = dependencies.get(0);
ViewCompat.offsetTopAndBottom(dependency, child.getHeight());
}
return true;
}
最终的效果就是这样的了:
图8. 列表默认布局在图片下方