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的大量运用来实现的。

一.创建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中绑定或者通过注释实现自动绑定。

1.通过代码绑定Behavior

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

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

2.在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_作为属性前缀。

3.自动绑定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。
注意: 在添加依赖时,视图总是会在依赖视图布局后进行布局,无视子项次序。

七.嵌套滚动

说到嵌套滚动,有详细介绍它的相关文章,在本文中笔者只做粗浅概述。需要牢记这几件事:
无需在嵌套滚动视图中声明依赖,因为CoordinatorLayout的每个子项都有可能接收到嵌套滚动事件。
嵌套滚动不仅可以在CoordinatorLayout的直接子项中发起,也能在任何子视图(比如CoordinatorLayout的子项的子项的子项中)发起。
虽然我们称之为嵌套滚动,不过实际上包括滚动(按照滚动做1:1的位移)与滑动(flinging)两种动作。

因此,通过onStartNestedScroll()来发起感兴趣的嵌套滚动事件吧。收到滚动轴(例如横向或纵向——使它容易忽略在特定方向上的滚动)后,必须在该方向上返回true,以获得随后的滚动事件。
在向onStartNestedScroll()返回true之后,嵌套滚动分两步运行:
onNestedPreScroll()在滚动视图获得滚动事件前运行,允许相应Behavior消耗一部分或所有的滚动事件(最后消耗的int[]是一个“外部”参数,在其中指明消耗掉的滚动)。
滚动视图在滚动后会调用onNestedScroll(),可以知道滚动了多少view,未消耗掉的(overscroll)数量又有多少。

还有类似滑动操作(尽管pre-fling调用必须要么消耗掉所有的滑动,要么不消耗滑动——没有部分消耗的选项)。
在嵌套滚动(或滑动)停止后,就能获得onStopNestedScroll()的调用。这表示滚动结束:在下一个滚动开始前,等待重新调用onStartNestedScroll()。
举个例子:如果想要在向下滚动时隐藏FloatingActionButton,并在向上滚动时显示它,只用重写onStartNestedScroll() 和onNestedScroll(),就像在这个ScrollAwareFABBehavior中看到的那样。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容