返回 登录
0

深入理解Android开发中的CoordinatorLayout Behavior

在使用Android设计支持库(Android Design Support Library)时,很难避开CoordinatorLayout:设计库中有很多视图都需要CoordinatorLayout的支持。为什么呢?实际上CoordinatorLayout本身所做的事情并不多,要是在标准框架视图中使用它,结果也就跟普通的FrameLayout差不多。那么奇迹来自何处呢?完全是由于CoordinatorLayout.Behaviors的存在。只要将Behavior绑定到CoordinatorLayout的直接子元素上,就能对触摸事件(touch events)、window insets、measurement、layout以及嵌套滚动(nested scrolling)等动作进行拦截。Design Library的大多功能都是借助Behavior的大量运用来实现的。

CoordinatorLayout 

创建Behavior

创建behavior非常简单:使用extend Behavior就可以了。

public class FancyBehavior<V extends View>
    extends CoordinatorLayout.Behavior<V> {
  /**
   * Default constructor for instantiating a FancyBehavior in code.
   */
  public FancyBehavior() {
  }
  /**
   * Default constructor for inflating a FancyBehavior from layout.
   *
   * @param context The {@link Context}.
   * @param attrs The {@link AttributeSet}.
   */
  public FancyBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
    // Extract any custom attributes out
    // preferably prefixed with behavior_ to denote they
    // belong to a behavior
  }
}

注意: 这里绑定了泛型类型,也就是说,可以将FancyBehavior绑定到任意视图类上。不过,如果只想将Behavior绑定到特定种类的视图上,就可以用这段代码:

public class FancyFrameLayoutBehavior
    extends CoordinatorLayout.Behavior<FancyFrameLayout>

这样一来,当从视图收到方法调用时,就无需再费神将大量参数转到正确的子类中了,简单又便捷。

使用Behavior.setTag()/Behavior.getTag() 可以保存临时数据, 使用onSaveInstanceState()/onRestoreInstanceState()还可以保存Behavior相关的实例状态。虽然笔者建议要保证Behavior尽可能轻量级,不过这些方法可以让Behavior更具状态性。

关联Behavior

当然,Behavior无法独立完成工作,必须与实际调用的CoordinatorLayout子视图相绑定。具体有三种方式:通过代码绑定、在XML中绑定或者通过注释实现自动绑定。

通过代码绑定Behavior

如果将Behavior当作绑定到CoordinatorLayout中每个视图的附加数据,那么发现Behavior实际上是存储在各个视图的LayoutParams中也就不足为奇了(之前有关于布局的博文)。也是因此,Behavior需要绑定到CoordinatorLayout的直接子项中,因为只有那些子项会包含LayoutParams的特定Behavior子类。

FancyBehavior fancyBehavior = new FancyBehavior();
CoordinatorLayout.LayoutParams params =
    (CoordinatorLayout.LayoutParams) yourView.getLayoutParams();
params.setBehavior(fancyBehavior);

在这种情况下,我们会使用默认的无参数构造函数,不过这并不代表有参数的构造函数就不能用了,反正想用代码编写什么都没有限制。

在XML中绑定Behavior

当然,每次都用代码绑定的话总是有些麻烦,正如大多自定义的LayoutParams一样,完成这件工作也有相应的layout_ 属性,这里是layout_behavior属性:

<FrameLayout
  android:layout_height=”wrap_content”
  android:layout_width=”match_parent”
  app:layout_behavior=”.FancyBehavior” />

与代码绑定不同,这里调用的总是FancyBehavior(Context context, AttributeSet attrs) 构造函数。此外还能声明任何自定义属性,并将其从XML AttributeSet中提取出来,如果想要赋予开发者通过XML自定义Behavior的功能,这一点非常重要。

注意: 与父类负责解析与诠释的Layout_属性的命名规则相类似,在Behavior中我们使用behavior_作为属性前缀。

自动绑定Behavior

如果构建了需要自定义Behavior的自定义视图(就像Design Library中很多组件中那样),也许你会想要默认绑定某个behavior,而无需每次手动在代码中或XML中指定。为了达到这个目的,只需在自定义视图顶层添加简单的注释:

@CoordinatorLayout.DefaultBehavior(FancyFrameLayoutBehavior.class)
public class FancyFrameLayout extends FrameLayout {
}

这样,默认的构造函数就会调用Behavior,与使用代码绑定非常类似。注意:目前任何layout_behavior代表的属性都会重写DefaultBehavior。

拦截触摸事件

一旦将所有behavior设置完毕,就可以准备实际开工了。Behavior能做的事情之一包括拦截触摸事件。

不用CoordinatorLayout时,一般会使用各个ViewGroup的子类,Managing Touch Events training一文有提到过这个问题。不过有了CoordinatorLayout,通过Behavior的onInterceptTouchEvent(),将调用传递给它的onInterceptTouchEvent(),让Behavior获得拦截触摸事件的机会。通过返回为true,那么Behavior会通过onTouchEvent()接收后续的所有触摸事件,而且无需视图了解后续情况。SwipeDismissBehavior就是通过这样的方式在视图中执行任务的。

不过更严重的触摸拦截就是拦截任何交互,只要在blocksInteractionBelow()中返回true就会出现这样的情况。当然,在互动被拦截时也许你会希望有些视觉信号提示(以免使用者以为应用完全不能用了)——这就是为什么blocksInteractionBelow()的默认功能实际上依赖于getScrimOpacity()值——返回非零值会为视图提供一层颜色遮罩(用getScrimColor()来确定颜色,默认为黑),并立即禁用所有的触摸互动。非常方便。

拦截window insets

假设本文读者已经看过Why would I want to fitsSystemWindows一文, 在该文中我们就fitsSystemWindows的实际作用做了深入探讨,不过可归结为:window insets需要避免在系统窗口(比如状态栏和导航栏)之下出现。这里Behavior也能发挥作用:如果视图为fitsSystemWindows=“true”,则onApplyWindowInsets()会调用绑定Behavior,且优先级高于视图自身。

注意: 大多情况下,如果Behavior没有消耗掉整个window insets,则应当通过ViewCompat.dispatchApplyWindowInsets() 来传递这个insets,以确保视图的任何子项有机会看到这个WindowInsets。

拦截Measurement和Layout

Measurement和layout是Android绘制视图的关键组件,因此Behavior只有在onMeasureChild()和onLayoutChild()回调前拦截父视图的measurement和layout,才能达到预计的效果。

例如:我们采用泛型ViewGroup并为其添加一个maxWidth:

CODE1
CODE2
CODE3

编写适用所有项目的通用Behavior非常有用,不过切记:尽量考虑在应用内使用behavior的办法,这样会让应用更为简单。(并非所有Behavior都应当是泛型的!)

理解视图间的依赖

上述所有功能都仅需要单个视图便可实现。不过Behavior的强大之处源自构建视图间的依赖,也就是说:当另一个视图改变时,你的Behavior会获得回调,根据外部情况来变更自身功能。

Behavior在两种情况下会成为视图的依赖:一种是将Behavior相应的视图锚定在另一个视图上时(隐性依赖),还有一种是在layoutDependsOn()中明确返回true时。

在视图中使用CoordinatorLayout的layout_anchor属性,就能起到锚定的作用。与layout_anchorGravity属性一同使用,就能将两个视图一并有效地固定在某个位置上。例如:可以将FloatingActionButton锚定到AppBarLayout上,而在AppBarLayout滚动出屏幕时,FloatingActionButton.Behavior就会通过隐性依赖将自身隐藏起来。

无论哪种情况,当依赖视图被移除时,Behavior会获得onDependentViewRemoved()的回调;而只要依赖视图出现变更,Behavior就会获得onDependentViewChanged()的回调(即调整大小或自身位置)。

将视图固定在一起的能力正是Design Library实现诸多炫酷功能的办法——比如FloatingActionButton与Snackbar之间的互动。FAB的Behavior依赖于添加到CoordinatorLayout上的Snackbar实例,再通过onDependentViewChanged()回调将FAB向上移动,避免遮住Snackbar。

注意: 在添加依赖时,视图总是会在依赖视图布局后进行布局,无视子项次序。

嵌套滚动

说到嵌套滚动,有详细介绍它的相关文章,在本文中笔者只做粗浅概述。需要牢记这几件事:

  1. 无需在嵌套滚动视图中声明依赖,因为CoordinatorLayout的每个子项都有可能接收到嵌套滚动事件。
  2. 嵌套滚动不仅可以在CoordinatorLayout的直接子项中发起,也能在任何子视图(比如CoordinatorLayout的子项的子项的子项中)发起。
  3. 虽然我们称之为嵌套滚动,不过实际上包括滚动(按照滚动做1:1的位移)与滑动(flinging)两种动作。

因此,通过onStartNestedScroll()来发起感兴趣的嵌套滚动事件吧。收到滚动轴(例如横向或纵向——使它容易忽略在特定方向上的滚动)后,必须在该方向上返回true,以获得随后的滚动事件。

在向onStartNestedScroll()返回true之后,嵌套滚动分两步运行:

  • onNestedPreScroll()在滚动视图获得滚动事件前运行,允许相应Behavior消耗一部分或所有的滚动事件(最后消耗的int[]是一个“外部”参数,在其中指明消耗掉的滚动)。
  • 滚动视图在滚动后会调用onNestedScroll(),可以知道滚动了多少view,未消耗掉的(overscroll)数量又有多少。

还有类似滑动操作(尽管pre-fling调用必须要么消耗掉所有的滑动,要么不消耗滑动——没有部分消耗的选项)。

在嵌套滚动(或滑动)停止后,就能获得onStopNestedScroll()的调用。这表示滚动结束:在下一个滚动开始前,等待重新调用onStartNestedScroll()。

举个例子:如果想要在向下滚动时隐藏FloatingActionButton,并在向上滚动时显示它,只用重写onStartNestedScroll() 和onNestedScroll(),就像在这个ScrollAwareFABBehavior中看到的那样。

这只是开始

Behavior的每个部分都很有趣,不过在把它们放在一起时,就出现了奇迹。笔者强烈建议,请读者查看Design Library的原代码库,找出更高级的behavior。Android SDK Search Chrome的扩展组件是我最喜欢搜索开源代码的来源之一(尽管包含在/extras/android/m2repository中的源码总是最新的)。

在深刻理解Behavior的功能之后,希望你们能使用它们构建更好的应用。

文章来源: Intercepting everything with CoordinatorLayout Behaviors
作者: Ian Lake,Android Developer Advocate
翻译: 孙薇
审校/责任编辑:唐小引,欢迎技术投稿、约稿,给文章挑错,请邮件发送至tangxy@csdn.net

第一时间掌握最新移动开发相关信息和技术,请关注mobilehub公众微信号(ID: mobilehub)。

mobilehub

评论