拦截一切的CoordinatorLayout Behavior

RosIrvin 9年前

来自: 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来实现你所看到的功能。

blob.png

创建一个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>   * &lt;declare-styleable name="MaxWidthBehavior_Params"&gt;   *     &lt;attr name="behavior_maxWidth" format="dimension"/&gt;   * &lt;/declare-styleable&gt;   * </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放置之后才会被放置。

嵌套滚动

啊哈,嵌套滚动。在这篇博客中,我只会点到为止。记住几点:

  1. 你不需要在嵌套滚动的View上面定义依赖。CoordinatorLayout的每个child都有机会接收到嵌套滚动事件。

  2. 嵌套滚动不仅可以开始于CoordinatorLayout的直接child,还可以开始于任何child(比如CoordinatorLayout的child的child)。

  3. 虽然我叫它嵌套滚动,但其实它包含滚动(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 !