拦截一切的CoordinatorLayout Behavior
来自: http://www.jcodecraeer.com//a/anzhuokaifa/androidkaifa/2016/0224/3991.html
原文:Intercepting everything with CoordinatorLayout Behaviors。
如果没有深入CoordinatorLayout ,你注定无法在探索Android Design Support Library的路上走多远 - Design Library中的许多view都需要一个CoordinatorLayout。但是为什么呢?CoordinatorLayout本身并没有做太多事情:和标准的framework视图一起使用时,它就跟一个普通的FrameLayout差不多。那么它的神奇之处来自于哪里呢?答案就是CoordinatorLayout.Behavior。通过为CoordinatorLayout的直接子view设置一个Behavior,就可以拦截touch events, window insets, measurement, layout, 和 nested scrolling等动作。Design Library大量利用了Behaviors来实现你所看到的功能。
创建一个Behavior
创建一个behavior很简单:继承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 } }
注意这个类设置的是普通View,这意味着你可以把FancyBehavior设置给任何View类。但是,如果你只允许让Behavior设置给一个特定类型的View,则需要这样写:
public class FancyFrameLayoutBehavior extends CoordinatorLayout.Behavior<FancyFrameLayout>
这可以省去把回调方法中收到的view参数转换成正确类型的步骤-效率第一嘛。
可以使用Behavior.setTag()/Behavior.getTag() 来保存临时数据,还可以使用onSaveInstanceState()/onRestoreInstanceState()来保存跟Behavior相关的实例的状态。我建议让Behaviors尽可能的轻,但是这些方法让状态化Behaviors成为可能。
设置Behavior
当然了,Behaviors并不会对自身做任何事情-它们需要被设置在一个CoordinatorLayout的子view上之后才会被实际调用。设置Behaviors主要有三种方式:程序中动态设置,xml布局文件设置和使用注解设置。
在程序中设置Behavior
当你认为Behavior是一个被设置在CoordinatorLayout每个子view上的附加数据时,你就不会对Behavior其实是保存在每个view的LayoutParam中感到奇怪了( 如果你已经阅读了我们 关于布局的文章 )- 这也是为什么Behaviors需要声明在CoordinatorLayout的直接子View上的原因,因为只有那些子View才存有CoordinatorLayout.LayoutParams(根据自己的理解翻译的)。
FancyBehavior fancyBehavior = new FancyBehavior(); CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) yourView.getLayoutParams(); params.setBehavior(fancyBehavior);
这里你会发现我们使用的是默认的无参构造函数。但这并不是说你就不能使用任何参数 - 如果你想,代码里面,万事皆有可能。
在xml里设置Behavior
当然,每次都在代码里面把所有事情做完会显得有点乱。就跟多数自定义的LayoutParam一样,这里也有相应的layout_ attribute 与之对应。那就是layout_behavior 属性:
<FrameLayout android:layout_height=”wrap_content” android:layout_width=”match_parent” app:layout_behavior=”.FancyBehavior” />
这里与前面不同的是,被调用的构造函数总是FancyBehavior(Context context, AttributeSet attrs)。因此,你可以在xml属性中声明你想要的其他自定义属性。如果你想让开发者能够通过xml自定义Behavior的功能,这点是很重要的。
注意:类似于由父类负责解析和解释的layout_ 属性命名规则,使用behavior_ prefix来指定被专门Behavior使用的某个属性。
例子(译者结合评论做的补充):
<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior=".MaxWidthBehavior" app:behavior_maxWidth="400dp" />
自动设置一个Behavior
如果你正在创建一个需要一个自定义Behavior的自定义View(就如Design Library中的许多控件那样),那么你很可能希望view默认就设置了那个Behavior,而不需要每次都通过xml或者代码去手动指定。为此,你只需在自定义View类的最上面设置一个简单的注解:
@CoordinatorLayout.DefaultBehavior(FancyFrameLayoutBehavior.class) public class FancyFrameLayout extends FrameLayout { }
你会发现你的Behavior会随着默认的构造函数被调用,这非常类似于与通过程序设置Behavior。注意任何 layout_behavior属性所代表的Behavior都会重写 DefaultBehavior。
拦截 Touch Events
一旦你设置好了所有的behavior,你就该准备做点实际工作了。Behavior能做的事情之一就是拦截触摸事件。
如果没有CoordinatorLayout,我们通常会被牵涉进 ViewGroup的子类中,就像 Managing Touch Events training一文所讨论的那样。但是如果有了CoordinatorLayout,CoordinatorLayout就会把它onInterceptTouchEvent() 中的参数(主要是MotionEvent)和调用传递到Behavior的onInterceptTouchEvent(),让你的Behavior有一次拦截触摸事件的机会。如果返回true,你的Behavior则会通过onTouchEvent() 收到所有的后续触摸事件-而View完全不知道发生了什么事情。这也是SwipeDismissBehavior 在view上的工作原理。
ps:我以前专门分析过SwipeDismissBehavior,和这段话基本一致。另外CoordinatorLayout其实是遍历了一遍自己的直接子View,一个一个的调用子view中的Behavior,见:SwipeDismissBehavior用法及实现原理 。
不过还有一个更粗暴的触摸拦截:拦截所有的交互。只需在 blocksInteractionBelow() 里返回true即可(我们这个视图下的其他视图将获取不到任何Touch事件)。当然,你可能希望在交互被阻止的情况下能有一些视觉效果 - 这就是为什么blocksInteractionBelow()实际上默认依赖 getScrimOpacity() 的值 - 返回一个非零将在View之上绘制一层overlay颜色并且屏蔽所有的交互。
拦截Window Insets
假设你读了Why would I want to fitsSystemWindows? blog。那里深入讨论了fitsSystemWindows到底干什么的,但是它归纳为:window insets 需要避免在 system windows(比如status bar 和 navigation bar)的下面绘制。
Behaviors在这里也有拦截的机会 - 如果你的View是fitsSystemWindows=“true”的,那么任何依附着的Behavior都将得到onApplyWindowInsets()调用,且优先级高于View自身。
注意:如果你的Behavior并没有消费掉整个 window insets,它应该通过 ViewCompat.dispatchApplyWindowInsets() 传递insets,以确保任何子view都能有机会看到这个WindowInsets。
拦截Measurement 和 layout
测量与布局(Measurement and layout)是 安卓如何绘制View的关键组成部分。因此对于能够拦截一切的Behavior来说,它应该能在第一时间拦截测量和布局才是合情合理的。 这要通过onMeasureChild() 和 onLayoutChild() 回调来完成。
比如, 我们找来任意一个普通的ViewGroup,并向它添加一个maxWidth:
/* * Copyright 2015 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.behaviors; import android.content.Context; import android.content.res.TypedArray; import android.support.design.widget.CoordinatorLayout; import android.util.AttributeSet; import android.view.ViewGroup; import static android.view.View.MeasureSpec; /** * Behavior that imposes a maximum width on any ViewGroup. * * <p />Requires an attrs.xml of something like * * <pre> * <declare-styleable name="MaxWidthBehavior_Params"> * <attr name="behavior_maxWidth" format="dimension"/> * </declare-styleable> * </pre> */ public class MaxWidthBehavior<V extends ViewGroup> extends CoordinatorLayout.Behavior<V> { private int mMaxWidth; public MaxWidthBehavior(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaxWidthBehavior_Params); mMaxWidth = a.getDimensionPixelSize( R.styleable.MaxWidthBehavior_Params_behavior_maxWidth, 0); a.recycle(); } @Override public boolean onMeasureChild(CoordinatorLayout parent, V child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { if (mMaxWidth <= 0) { // No max width means this Behavior is a no-op return false; } int widthMode = MeasureSpec.getMode(parentWidthMeasureSpec); int width = MeasureSpec.getSize(parentWidthMeasureSpec); if (widthMode == MeasureSpec.UNSPECIFIED || width > mMaxWidth) { // Sorry to impose here, but max width is kind of a big deal width = mMaxWidth; widthMode = MeasureSpec.AT_MOST; parent.onMeasureChild(child, MeasureSpec.makeMeasureSpec(width, widthMode), widthUsed, parentHeightMeasureSpec, heightUsed); // We've measured the View, so CoordinatorLayout doesn't have to return true; } // Looks like the default measurement will work great return false; } }
写一个通用的Behavior固然有用,但我们需要知道的是有时候如果你想让你的app简单一点的话完全可以把Behavior的相关功能写在自定义View的内部,没必要为了使用Behavior而是用它。
理解View之间的依赖
以上的所有功能都只需要一个View。但是Behaviors的强大之处在于在View之间建立依赖关系-当另一个View改变的时候,你的Behavior会得到一个callback,根据外部条件改变它的功能。
Behaviors依赖于View有两种形式:当它的View锚定于另外一个View(一种隐式的依赖)或者,当你在layoutDependsOn()中明确的返回true。
锚定发生于你使用了CoordinatorLayout的layout_anchor 属性之时。它和layout_anchorGravity 属性结合,可以让你有效的把两个View捆绑在一起。比如,你可以把一个FloatingActionButton锚定在一个AppBarLayout上,那么如果AppBarLayout滚动出屏幕,FloatingActionButton.Behavior将使用隐式的依赖去隐藏FAB。
不管什么形式,当一个依赖的View被移除的时候你的Behavior会得到回调 onDependentViewRemoved() ,当依赖的View发生变化的时候(比如:调整大小或者重置自己的position),得到回调 onDependentViewChanged()
这个把View绑定在一起的能力正是Design Library那些酷炫功能的工作原理 -以FloatingActionButton与Snackbar之间的交互为例。FAB的 Behavior依赖于被添加到CoordinatorLayout的Snackbar,然后它使用onDependentViewChanged() callback来将FAB向上移动,以避免和Snackbar重叠。
注意:如果你添加了一个依赖,不管child的顺序如何,你的View将总是在所依赖的View放置之后才会被放置。
嵌套滚动
啊哈,嵌套滚动。在这篇博客中,我只会点到为止。记住几点:
-
你不需要在嵌套滚动的View上面定义依赖。CoordinatorLayout的每个child都有机会接收到嵌套滚动事件。
-
嵌套滚动不仅可以开始于CoordinatorLayout的直接child,还可以开始于任何child(比如CoordinatorLayout的child的child)。
-
虽然我叫它嵌套滚动,但其实它包含滚动(scrolling)和划动(flinging)两种。
那么让我们使用onStartNestedScroll()来定义你所感兴趣的嵌套滚动(方向)。你将收到滚动的轴(比如横向或者纵向-让它可以轻易的忽略某个方向上的滚动)并且为了接收那个方向上的后续滚动事件必须返回true。
当你在onStartNestedScroll()中返回了true之后,嵌套滚动进入两个阶段:
-
onNestedPreScroll() 会在scrolling View获得滚动事件前调用,它允许你消费部分或者全部的事件信息。
-
onNestedScroll() 会在scrolling View做完滚动后调用,通过回调可以知道scrolling view滚动了多少和它没有消耗的滚动事件。
同样,fling操作也有与之相对应的方法(虽然e pre-fling callback 必须消费完或者完全不消费fling - 没有消费部分的情况)。
当嵌套滚动(或者flinging)结束,你将得到一个onStopNestedScroll()回调。这标志着滚动的结束 - 迎接在下一个滚动之前的onStartNestedScroll() 调用。
比如,当向下滚动的时候隐藏FloatingActionButton,向上滚动的时候显示FloatingActionButton- 这只牵涉到重写onStartNestedScroll() 和 onNestedScroll(),就如在ScrollAwareFABBehavior中所看到的那样。
这只是开始
Behavior每个单独的部分都很有趣,当他们结合起来就会发生很神奇的事情。为了了解更多的高级behavior,我强烈鼓励你去查看Design Library的源码-Android SDK Search Chrome extension是我探索AOSP源码时最喜欢的资源(虽然包含在 <android-sdk>/extras/android/m2repository中的源码总是最新的)。
在了解Behavior能做哪些事情这点上打下了坚实的基础后,让我知道你们是如何使用它们创建更优秀的app的。
要了解更多,请参与在 Google+ post 上的讨论并关注 Android Development Patterns Collection !