转载

拦截一切的CoordinatorLayout Behavior

原文: Intercepting everything with CoordinatorLayout Behaviors 。  这篇文章适合已经了解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来实现你所看到的功能。

拦截一切的CoordinatorLayout Behavior

创建一个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主要有三种方式: 程序中动态设置 ,x ml布局文件设置 使用注解设置

在程序中设置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放置之后才会被放置。

嵌套滚动

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

那么让我们使用 onStartNestedScroll() 来定义你所感兴趣的嵌套滚动(方向)。你将收到滚动的轴(比如横向或者纵向-让它可以轻易的忽略某个方向上的滚动)并且为了接收那个方向上的后续滚动事件必须返回true。

当你在onStartNestedScroll()中返回了true之后,嵌套滚动进入两个阶段:

同样,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 !

原文  http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2016/0224/3991.html
正文到此结束
Loading...