CoordinatorLayout Behavior 详解

如果你要探索 Android Design Support Library ,那么你一定会接触到 CoordinatorLayout ,因为许多 Design Library 的视图都需要 CoordinatorLayout. 为什么呢?CoordinatorLayout 本身并没有做什么:其实它就是一个 FrameLayout. 那么, CoordinatorLayout 的炫酷效果要怎么实现的呢?其实 CoordinatorLayout 是依赖 CoordinatorLayout.Behavior 来实现的。通过给 CoordinatorLayout 的子布局添加 Behavior ,我们可以拦截触摸事件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,可以这么写:

public class FancyFrameLayoutBehavior
extends CoordinatorLayout.Behavior<FancyFrameLayout>

这样可以把你从View接收到的参数转化成正确的类型,默认方式会全部转换。

通过Behavior.setTag()/Behavior.getTag()方法,结合onSaveInstanceState()/onRestoreInstanceState()可以存储临时数据。我们建议你在创建Behavior时尽量轻量一些,不过这些方法为我们提供了创建可保存状态的 Behavior的可能。

###绑定行为(Attaching a Behavior)
当然,Behavior需要绑定在CoordinatorLayout的子视图上并且确认被调用,否则它没有任何效果。有三种完成绑定的方式:通过代码、通过 XML或者通过注解

####通过代码绑定Behavior(Attaching a Behavior programmatically)
当你想把Behavior绑定在CoordinatorLayout的子视图中,你一定要知道Behavior是存储在每个视图的 LayoutParams 中的 - Behavior 必须声明在 CoordinatorLayout 的直系的子视图,因为只有这些子视图拥有 LayoutParams 的明确的 Behavior 基类。

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

上面这个例子中,我们使用默认的,无参数的构造函数。这并不代表你不能使用构造函数来传递参数-当你通过代码完成这些的时候,你是不受限的。

####通过 XML 绑定 Behavior(Attaching a Behavior in XML)
每次都需要通过代码设置,会有一点乱。就像大多数自定义的LayoutParams一样,有对应的layout_attribute来做相同的事。下面这个例子,是使用la yout_behavior属性:

<FrameLayout
  android:layout_height=”wrap_content”
  android:layout_width=”match_parent”
  app:layout_behavior=”.FancyBehavior” />

与通过代码设置不同,FancyBehavior(Context context, AttributeSet attrs)构造函数会默认被调用,当然,你可以通过XMLAttributeSet来声明其他属性(如果你需要其他开发者通过 XML 修改你的 Behavior 的方法)。

相似的layout_naming属性命名方式,可以帮助我们在代码中更好的理解和解析,推荐为所有声明的Behavior属性使用behavior_前缀。

####通过注解自动绑定 Behavior(Attaching a Behavior automatically)

如果你创建一个需要自定义 Behavior 的自定义视图(就像很多 Design Library 提供的很多效果的例子),你可能希望默认绑定一些行为,不需要每次都使用代码或者 XML 配置。想做到这些,你的自定义视图只需要在它的类的第一行加上一个简单地注释:

@CoordinatorLayout.DefaultBehavior(FancyFrameLayoutBehavior.class)
public class FancyFrameLayout extends FrameLayout {
}

上述代码将会调用 Behavior 的默认构造函数,这样与代码绑定Behavior非常相似。注意:所有layout_behavior属性,都需要覆盖DefaultBehavior

###监听触摸事件(Intercepting Touch Events)

一旦你配置完成你的 behavior,就可以做一些对应的操作。Behavior 的众多功能之一就是监听触摸事件。

在没有CoordinatorLayout时,我们都需要了解ViewGroup管理触摸事件。然而,有了CoordinatorLayout后,CoordinatorLayout会把它的onInterceptTouchEvent()自动传给你定义的BehavioronInterceptTouchEvent()你的 Behavior 可以监听 Touch 事件。如果把返回值设定为 true,自定义的 Behavior 将会接收到 onTouchEvent() 的全部触摸事件(此时触摸事件被拦截,其他视图都不接收到触摸事件)。例如:SwipeDismissBehavior对所有视图都有效。

另外,如果 blocksInteractionBelow() 返回 true,则可以切断任何视图间的互相交流。当然,你需要通过给用户提供一些视觉信息,来告诉用户动画的交互效果已经结束(以免用户以为程序挂掉了)。blocksInteractionBelow()方法的默认返回值依赖于getScrimOpacity() 方法,如果getScrimOpacity()方法返回一个不为零的值时,CoordinatorLayout 将会在视图的上层绘制一个遮罩的颜色(色值来自getScrimColor(),默认颜色是黑色)并且停止触摸事件。

监听窗口插入(Intercepting Window Insets)

假设你阅读过 我为什么使用 fitsSystemWindows 这篇博客。我们深入的讲解了 fitsSystemWindows 到底做了什么,归根结底就是告诉你窗口Window要避免插入到系统窗体(比如: status bar 和 navigation bar)的下层。Behaviors 有它自己的处理方式(如果视图使用了fitsSystemWindows=”true”,那么任何绑定的 Behavior 都会调用 onApplyWindowInsets()来给它优先展示在视图最上层的权利)

注:大多数情况下, Behavior 没有占据整个窗体,它需要通过ViewCompat.dispatchApplyWindowInsets() 来确保所有子视图在窗体中是可见的。

####监听测量和布局(Intercepting Measurement and Layout)
测量(Measurement)和布局(Layout)是绘制视图的重要部分。Behaviors也因此变得有意义,所有的监听事件,都通过onMeasureChild()onLayoutChild()的回调获取第一次 measurement 和 layout 信息。

例如,我们创建一个有最大宽度限制的通用 ViewGroup :

/*
 * 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 很方便,但是根据你应用的需求来定义,不是所有的 Behavior 都需要写成通用的。

###未完待续…

原文链接:https://medium.com/google-developers/intercepting-everything-with-coordinatorlayout-behaviors-8c6adc140c26#.f9om9s91r