Android 一步一步分析CoordinatorLayout.
在MD系列的前几篇文章中,通过基础知识和实战案例配合讲解的形式介绍了CoordinatorLayout
与AppBarLayout
、Toolbar
、CollapsingToolbarLayout
的使用,并实现了几种MD风格下比较炫酷的交互效果。学会怎么用之后,我们再想想,为什么它们之间能够产生这样的交互行为呢?其实就是因为CoordinatorLayout.Behavior
的存在,这也是本文所要讲述的内容。至此,Android Material Design系列的学习已进行到第八篇,大家可以点击以下链接查看之前的文章:
- Android TabLayout 分分钟打造一个滑动标签页
- Android 一文告诉你到底是用Dialog,Snackbar,还是Toast
- Android FloatingActionButton 重要的操作不要太多,一个就好
- Android 初识AppBarLayout 和 CoordinatorLayout
- Android CoordinatorLayout实战案例学习《一》
- Android CoordinatorLayout 实战案例学习《二》
- Android 详细分析AppBarLayout的五种ScrollFlags
关于Behavior
官网对于CoordinatorLayout.Behavior
的介绍已经将它的作用说明得很清楚了,就是用来协调CoordinatorLayout
的Child Views之间的交互行为:
Interaction behavior plugin for child views of CoordinatorLayout.
A Behavior implements one or more interactions that a user can take on a child view. These interactions may include drags, swipes, flings, or any other gestures.
之前学习CoordinatorLayout
的使用案例时,用的都是系统的特定控件,比如design包中的FloatingActionButton
、AppBarLayout
等,而不是普通的控件,如ImageButton
之类的,就是因为design包中的这些特定控件已经被系统默认定义了继承自CoordinatorLayout.Behavior
的各种Behavior
,比如FloatingActionButton.Behavior
和
AppBarLayout.Behavior
。而像系统的ToolBar
控件就没有自己的Behavior
,所以只能将其搁置到AppBarLayout
容器里才能产生相应的交互效果。
看到这里就能清楚一点了,如果我们想实现控件之间任意的交互效果,完全可以通过自定义Behavior
的方式达到。看到这里大家可能会有一个疑惑,就是CoordinatorLayout
如何获取Child Views的Behavior
的呢,为什么在布局中,有些滑动型控件定义了app:layout_behavior
属性而系统类似FloatingActionButton
的控件则不需要明确定义该属性呢?看完CoordinatorLayout.Behavior
的构造函数就明白了。
/**
* Default constructor for instantiating Behaviors.
*/
public Behavior() {
}
/**
* Default constructor for inflating Behaviors from layout. The Behavior will have
* the opportunity to parse specially defined layout parameters. These parameters will
* appear on the child view tag.
*
* @param context
* @param attrs
*/
public Behavior(Context context, AttributeSet attrs) {
}
CoordinatorLayout.Behavior
有两个构造函数,注意看第二个带参数的构造函数的注释,里面提到,在这个构造函数中,Behavior
会解析控件的特殊布局属性,也就是通过parseBehavior
方法获取对应的Behavior
,从而协调Child Views之间的交互行为,可以在CoordinatorLayout
类中查看,具体源码如下:
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
final String fullName;
if (name.startsWith(".")) {
// Relative to the app package. Prepend the app package name.
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
// Fully qualified package name.
fullName = name;
} else {
// Assume stock behavior in this package (if we have one)
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
? (WIDGET_PACKAGE_NAME + '.' + name)
: name;
}
try {
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor<Behavior> c = constructors.get(fullName);
if (c == null) {
final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
context.getClassLoader());
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
parseBehavior
方法告诉我们,给Child Views设置Behavior
有两种方式:
-
app:layout_behavior
布局属性
在布局中设置,值为自定义Behavior
类的名字字符串(包含路径),类似在AndroidManifest.xml
中定义四大组件的名字一样,有两种写法,包含包名的全路径和以"."开头的省略项目包名的路径。 -
@CoordinatorLayout.DefaultBehavior
类注解
在需要使用Behavior
的控件源码定义中添加该注解,然后通过反射机制获取。这个方式就解决了我们前面产生的疑惑,系统的AppBarLayout
、FloatingActionButton
都采用了这种方式,所以无需在布局中重复设置。
看到这里,也告诉我们一点,在自定义Behavior
时,一定要重写第二个带参数的构造函数,否则这个Behavior
是不会起作用的。
根据CoordinatorLayout.Behavior
提供的方法,这里将自定义Behavior
分为两类来讲解,一种是dependent
机制,一种是nested
机制,对应着不同的使用场景。
dependent
机制
这种机制描述的是两个Child Views之间的绑定依赖关系,设置Behavior
属性的Child View跟随依赖对象Dependency View的大小位置改变而发生变化,对应需要实现的方法常见有两个:
/**
* Determine whether the supplied child view has another specific sibling view as a
* layout dependency.
*
* <p>This method will be called at least once in response to a layout request. If it
* returns true for a given child and dependency view pair, the parent CoordinatorLayout
* will:</p>
* <ol>
* <li>Always lay out this child after the dependent child is laid out, regardless
* of child order.</li>
* <li>Call {@link #onDependentViewChanged} when the dependency view's layout or
* position changes.</li>
* </ol>
*
* @param parent the parent view of the given child
* @param child the child view to test
* @param dependency the proposed dependency of child
* @return true if child's layout depends on the proposed dependency's layout,
* false otherwise
*
* @see #onDependentViewChanged(CoordinatorLayout, android.view.View, android.view.View)
*/
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
和
/**
* Respond to a change in a child's dependent view
*
* <p>This method is called whenever a dependent view changes in size or position outside
* of the standard layout flow. A Behavior may use this method to appropriately update
* the child view in response.</p>
*
* <p>A view's dependency is determined by
* {@link #layoutDependsOn(CoordinatorLayout, android.view.View, android.view.View)} or
* if {@code child} has set another view as it's anchor.</p>
*
* <p>Note that if a Behavior changes the layout of a child via this method, it should
* also be able to reconstruct the correct position in
* {@link #onLayoutChild(CoordinatorLayout, android.view.View, int) onLayoutChild}.
* <code>onDependentViewChanged</code> will not be called during normal layout since
* the layout of each child view will always happen in dependency order.</p>
*
* <p>If the Behavior changes the child view's size or position, it should return true.
* The default implementation returns false.</p>
*
* @param parent the parent view of the given child
* @param child the child view to manipulate
* @param dependency the dependent view that changed
* @return true if the Behavior changed the child view's size or position, false otherwise
*/
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
具体含义在注释中已经很清楚了,layoutDependsOn()
方法用于决定是否产生依赖行为,onDependentViewChanged()
方法在依赖的控件发生大小或者位置变化时产生回调。dependent
机制最常见的案例就是FloatingActionButton
和SnackBar
的交互行为,效果如下:
系统的FloatingActionButton
已经默认定义了一个Behavior
来协调交互,如果不用系统的FAB控件,比如改用GitHub上的一个库futuresimple/android-floating-action-button
,再通过自定义一个Behavior
,也能很简单的实现与SnackBar
的协调效果:
package com.yifeng.mdstudysamples;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.util.AttributeSet;
import android.view.View;
/**
* Created by yifeng on 16/9/20.
*
*/
public class DependentFABBehavior extends CoordinatorLayout.Behavior {
public DependentFABBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 判断依赖对象
* @param parent
* @param child
* @param dependency
* @return
*/
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
/**
* 当依赖对象发生变化时,产生回调,自定义改变child view
* @param parent
* @param child
* @param dependency
* @return
*/
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
child.setTranslationY(translationY);
return true;
}
}
很简单的一个自定义Behavior
处理,然后再为对应的Child View设置该属性即可。由于这里我们用的是第三方库,采用远程依赖的形式引入的,无法修改源码,所以不方便使用注解的方式为其设置Behavior
,所以在布局中为其设置,并且使用了省略包名的方式:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<include
layout="@layout/include_toolbar"/>
</android.support.design.widget.AppBarLayout>
<com.getbase.floatingactionbutton.FloatingActionButton
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:id="@+id/fab_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/dp_16"
android:layout_gravity="bottom|right"
android:onClick="onClickFab"
fab:fab_icon="@mipmap/ic_toolbar_add"
fab:fab_colorNormal="?attr/colorPrimary"
fab:fab_colorPressed="?attr/colorPrimaryDark"
app:layout_behavior=".DependentFABBehavior"/>
</android.support.design.widget.CoordinatorLayout>
这样,采用dependent
机制自定义Behavior
,与使用系统FAB按钮一样,即可与SnackBar
控件产生如上图所示的协调交互效果。
比如我们再看一下这样一个效果:
Behavior-03列表上下滑动式,底部评论区域随着顶部Toolbar
的移动而移动,这里我们就可以自定义一个Dependent
机制的Behavior
,设置给底部视图,让其依赖于包裹Toolbar
的AppBarLayout
控件:
package com.yifeng.mdstudysamples;
import android.content.Context;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.View;
/**
* Created by yifeng on 16/9/23.
*
*/
public class CustomExpandBehavior extends CoordinatorLayout.Behavior {
public CustomExpandBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof AppBarLayout;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
int delta = dependency.getTop();
child.setTranslationY(-delta);
return true;
}
}
布局内容如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_56"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<include
layout="@layout/include_toolbar"/>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_56"
android:layout_gravity="bottom"
app:layout_behavior=".CustomExpandBehavior"
android:padding="8dp"
android:background="@color/blue">
<Button
android:id="@+id/btn_send"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="Send"
android:layout_alignParentRight="true"
android:background="@color/white"/>
<EditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toLeftOf="@id/btn_send"
android:layout_marginRight="4dp"
android:padding="4dp"
android:hint="Please input the comment"
android:background="@color/white"/>
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>
注意,这里将自定义的Behavior
设置给了底部内容的外层容器RelativeLayout
,即可实现上述效果。
Nested
机制
Nested
机制要求CoordinatorLayout
包含了一个实现了NestedScrollingChild
接口的滚动视图控件,比如v7包中的RecyclerView
,设置Behavior
属性的Child View会随着这个控件的滚动而发生变化,涉及到的方法有:
onStartNestedScroll(View child, View target, int nestedScrollAxes)
onNestedPreScroll(View target, int dx, int dy, int[] consumed)
onNestedPreFling(View target, float velocityX, float velocityY)
onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
onStopNestedScroll(View target)
其中,onStartNestedScroll
方法返回一个boolean类型的值,只有返回true时才能让自定义的Behavior
接受滑动事件。同样的,举例说明一下。
通过查看系统FAB控件的源码可以知道,系统FAB定义的Behavior
能够处理两个交互,一个是与SnackBar
的位置交互,效果如上面的图示一样,另一个就是与AppBarLayout
的展示交互,都是使用的Dependent
机制,效果在之前的文章 -- Android CoordinatorLayout 实战案例学习《二》 中可以查看,也就是AppBarLayout
滚动到一定程度时,FAB控件的动画隐藏与展示。下面我们使用Nested
机制自定义一个Behavior
,实现如下与列表协调交互的效果:
为了能够使用系统FAB控件提供的隐藏与显示的动画效果,这里直接继承了系统FAB控件的Behavior
:
package com.yifeng.mdstudysamples;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
/**
* Created by yifeng on 16/8/23.
*
*/
public class NestedFABBehavior extends FloatingActionButton.Behavior {
public NestedFABBehavior(Context context, AttributeSet attrs) {
super();
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
nestedScrollAxes);
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
//系统FAB控件提供的隐藏动画
child.hide();
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
//系统FAB控件提供的显示动画
child.show();
}
}
}
然后在布局中添加RecyclerView
,并为系统FAB控件设置自定义的Behavior
,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<include
layout="@layout/include_toolbar"/>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_content"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/dp_16"
android:src="@mipmap/ic_toolbar_add"
app:layout_anchor="@id/rv_content"
app:layout_anchorGravity="bottom|right"
app:backgroundTint="@color/fab_ripple"
app:layout_behavior="com.yifeng.mdstudysamples.NestedFABBehavior"/>
</android.support.design.widget.CoordinatorLayout>
这样,即可实现系统FAB控件与列表滑动控件的交互效果。
@string/appbar_scrolling_view_behavior
这是一个系统字符串,值为:
android.support.design.widget.AppBarLayout$ScrollingViewBehavior
在CoordinatorLayout
容器中,通常用在AppBarLayout
视图下面(不是里面)的内容控件中,比如上面的RecyclerView
,如果我们不给它添加这个Behavior
,Toolbar
将覆盖在列表上面,出现重叠部分,如图
添加之后,RecyclerView
将位于Toolbar
下面,类似在RelativeLayout
中设置了below
属性,如图:
示例源码
我在GitHub上建立了一个Repository,用来存放整个Android Material Design系列控件的学习案例,会伴随着文章逐渐更新完善,欢迎大家补充交流,Star地址: