为了账号安全,请及时绑定邮箱和手机立即绑定

拦截一切的CoordinatorLayout Behavior

标签:
Android

如果没有深入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的。

原文链接:http://www.apkbus.com/blog-873055-77902.html

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消