CoordinatorLayout 从诞生之初起,因其特殊的"协调"手段,非常适用于实现复杂的视图交互和动画,比如响应滚动事件、展开或折叠工具栏(Toolbar)等,一直以来被广大Android开发者津津乐道。而实现这一关键效果,正是仰仗CoordinatorLayout.Behavior
。本文尝试追本溯源,详细讲讲Behavior到底是个啥?我该怎么用?
Behavior是什么
Android团队是这么定义Behavior的:
Behavior:CoordinatorLayout 子视图的交互行为插件。Behavior 实现了用户可以对子视图进行的一个或多个交互操作。这些交互操作可能包括拖拽、滑动、甩动或任何其他手势。
Behavior是一个抽象类,除开构造方法外,其主要包含以下几个方法:
onAttachedToLayoutParams(CoordinatorLayout.LayoutParams params)
和onDetachedFromLayoutParams()
对称式API,CoordinatorLayout
调用setBehavior()
方法设置behavior 时被调用,通知对应的behavior它所持有的LayoutParams
实例已被 绑定/分离 。
onInterceptTouchEvent
/onTouchEvent
熟悉事件分发机制应该懂这两个Api的含义:是否要拦截触摸事件 以及 如何消费事件。
getScrimColor(CoordinatorLayout parent, V child)
/getScrimOpacity(CoordinatorLayout parent, V child)
介绍这两个API前,先介绍一个前端交互上常见的概念,遮罩(Scrim):UI设计为了凸显用户当前操作/交互的元素时,往往会给该元素上面或下面盖上一层带颜色的图层,这个图层我们称之为Scrim
。CoordinatorLayout作为侧重交互效果的布局容器,自然也有这个特性,对应的API就是behavior中的这两个方法,一个设置遮罩颜色,一个设置遮罩的透明度。CoordiantorLayout中的遮罩位于元素下方作为背景呈现。
public boolean blocksInteractionBelow(CoordinatorLayout parent, V child)
从方法名不难看出,该方法是用来决定是否要阻止位于给定child视图下方的View的交互行为的,默认当getScrimOpacity()
大于0时,拦截该child视图下方的View的交互行为。
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency)
指定一个视图是否依赖于 CoordinatorLayout 中的另一个视图。通过这种方式,开发者可以定义复杂的布局行为和交互模式,其中一个视图的状态或位置变化可以影响到另一个视图。三个参数分别对应父 CoordinatorLayout
的实例、子视图(child
)的实例以及另一个可能被依赖的视图(dependency
)的实例。它返回一个布尔值,true
则表示子视图依赖于指定的视图。
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency)
layoutDependsOn 方法并返回 true 表示存在依赖关系时,每当依赖视图的位置、大小或其他属性发生变化,onDependentViewChanged 方法就会被调用。这为开发者提供了一个机会,可以根据依赖视图的变化来调整当前视图(child view)的状态、位置等属性。 返回一个 boolean 值,表示子视图是否因为依赖视图的变化而发生了改变。如果返回 true,则表示子视图发生了改变,这将导致布局的再次计算和重绘。如果返回 false,则表示子视图没有因依赖视图的变化而发生改变,不需要重新布局或绘制。
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency)
与onDependentViewChanged
方法类似,不同的是该方法在依赖视图从CoordinatorLayout
中移除时被调用。这个回调提供了一个机会,让开发者可以响应依赖视图的移除事件,执行一些清理工作或者更新当前视图(child view)的状态、位置等。
onMeasureChild() / onLayoutChild()
这两个方法用于在测量阶段允许开发者介入子视图(即行为关联的视图)的测量/布局过程。通过重写这两个方法,可以自定义子视图的测量/布局逻辑,从而实现特定的布局需求或调整。
Behavior中同样可以对嵌套滚动进行处理,包含了
onStartNestedScroll
、onStartNestedScroll
、onNestedScrollAccepted
、onStopNestedScroll
、onNestedScroll
、onNestedPreScroll
、onNestedFling
、onNestedPreFling
等方法,他们的含义和NestedScrollingParent2
和NestedScrollingParent3
接口中的含义一致,此处不做额外介绍。
Android中内置的Behavior
Android中预定义几种Behavior,用以实现各种复杂的交互逻辑,以下是一些常见的内置Behavior
:
-
FloatingActionButton.Behavior:这个行为是为 FloatingActionButton(浮动操作按钮)设计的。它能够响应 Snackbar 的出现,自动移动浮动按钮以避免遮挡 Snackbar。
-
SwipeDismissBehavior:这是一个通用行为,可以用于任何 View。它为视图添加了滑动以消失的交互模式,类似于 Snackbar 的滑动消失效果。
-
AppBarLayout.ScrollingViewBehavior:这个行为专为与 AppBarLayout 一起使用的滚动视图(如 RecyclerView 或 NestedScrollView)设计。它确保滚动视图和 AppBarLayout 的滚动行为协同工作,实现材料设计的滚动效果。
-
AppBarLayout.Behavior:这是 AppBarLayout 的默认行为,支持滚动手势,使得 AppBarLayout 可以响应内容滚动,实现折叠工具栏的效果。
-
BottomSheetBehavior:这个行为用于 BottomSheet,即从屏幕底部滑出的面板。BottomSheetBehavior 允许用户通过拖动或滑动来展开或隐藏底部面板。
-
CollapsingToolbarLayout.Behavior:这是为 CollapsingToolbarLayout 设计的行为,它可以在滚动时实现复杂的折叠动画效果,通常与 AppBarLayout 结合使用。
-
... ...
介绍完了API,可以尝试一下自定义Behavior练练手。
定制自己的Behavior
作为CoordinatorLayout
协调布局实现的关键,Behavior实现的交互效果大致可以分为两类:
- 协调子View之间的关系:重写
layoutDependsOn
和onDependentViewChanged
等方法,建立View间的依赖关系,每当被依赖视图(dependency)的位置、大小或其他属性发生变化时调整当前视图(child)的状态、位置等属性。 - 协调嵌套嵌套滑动:重写嵌套滑动相关API实现不同的交互。
接下来我们从具体需求出发,一个一个来看。
协调子View之间的关系:避让BottomSheetBehavior的Behavior
我们来实现这样一个Behavior
:当设置了BottomSheetBehavior的View弹出时,自动避让该View(dependency),隐藏View(child)。 代码如下:
kotlin
/**
* 根据BottomSheetBehavior控制View的显示隐藏以避让BottomSheet
*/
class VisibilityByBottomSheetBehavior(
context: Context,
attributeSet: AttributeSet,
): CoordinatorLayout.Behavior<View>(context, attributeSet) {
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
// 如果依赖的View是BottomSheet,则建立依赖关系
return isBottomSheet(dependency)
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
if (isBottomSheet(dependency)) {
// 如果依赖的View的顶部位置大于等于child的底部位置,则隐藏child
child.isVisible = dependency.top >= child.bottom
}
// 返回false,布局的不会再次计算和重绘
return false
}
private fun isBottomSheet(view: View): Boolean {
val lp = view.layoutParams
return if (lp is CoordinatorLayout.LayoutParams) {
lp.behavior is BottomSheetBehavior<*>
} else false
}
}
实现的效果如下:
简简单单几行代码,就实现了根据BottomSheetBehavior控制View的显示隐藏,达到避让BottomSheet的目的。
协调嵌套滑动
如果熟悉Android中嵌套滚动的相关接口(NestedScrollingParent
、 NestedScrollingChild
)的话,看到CoordinatorLayout.Behavior中的onStartNestedScroll
、onStartNestedScroll
、onNestedScrollAccepted
、onStopNestedScroll
、onNestedScroll
、onNestedPreScroll
、onNestedFling
、onNestedPreFling
等方法一定不陌生,没错,CoordinatorLayout也是基于这套机制通过Behavior来协调嵌套滑动的。 不熟悉这套机制的,可以先移步熟悉这套机制:
NestedScrollParent2
自定义Behavior实现各种炫酷交互
熟悉了Behavior基础用法后,就可以使用灵活运动这套机制实现各种炫酷交互,网上相关案例也非常多,基本都是仿写各种炫酷交互,这里引用几个:
通过来模仿稀土掘金个人页面的布局来学习使用 CoordinatorLayout
一起动才够嗨!Android CoordinatorLayout 自定义 Behavior
自定义 Behavior,实现嵌套滑动、平滑切换周月视图的日历
以后UI交互提出什么复杂的交互效果时,不妨换个思路,尝试自定义Behavior来实现,也许能事半功倍。