如果你要探索 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)
构造函数会默认被调用,当然,你可以通过XML
的AttributeSet
来声明其他属性(如果你需要其他开发者通过 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()
自动传给你定义的Behavior
的onInterceptTouchEvent(),你的 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>
* <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 很方便,但是根据你应用的需求来定义,不是所有的 Behavior 都需要写成通用的。
###未完待续…