CoordinatorLayout.Behavior使用指南

1. Behavior 介绍

Behavior是CoordinatorLayout的子类,用于处理各个子view之间的行为(
这就意味着需要联动的View得用CoordinatorLayout包起来 ),例如:手指向下滑动的时候是View A下移,还是View
B下移,亦或者给View C来一个旋转?一个例子就是Bilibili客户端视频播放页的滑动效果,如图1所示:

图1.
Bilinili客户端的滑动效果

图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

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步:

  1. 判断依赖关系,即谁依赖谁
  2. 当被依赖的view发生了变化时对依赖view进行变化
  3. 处理嵌套滚动事件
4.2.1 判断依赖关系,即谁依赖谁

假设A依赖B,则Behavior应该设置在A上面,然后在layoutDependsOn方法里面,判断如果dependency为B则返回true。

4.2.2 当被依赖的view发生了变化时对依赖view进行变化

dependency的位置、方向等变化之后会调用onDependentViewChanged方法,在这个方法里面,可以对view进行跟随变化,比如将view对bottom设置为dependencytop位置,则view会始终在dependency的上面,高度跟随dependency动态变化。如果需要在dependency被移除的时候对view进行变化,则重写onDependentViewRemoved即可。

4.2.3 处理嵌套滚动事件

处理一个滚动事件的流程是:

  1. 判断滑动方向是否是自己需要的

  2. 处理滑动值

  3. 滚动事件结束是否需要回弹动画

与此对应的三个方法是:

  1. 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;
  1. 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了。

  1. 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. 列表默认布局在图片下方

Search by:GoogleBingBaidu