Fork me on GitHub

一文彻底搞懂 CoordinatorLayout 和 AppbarLayout 联动,让 Design 设计更简单

一、写在前面

其实博主在之前已经对 Design 包的各个控件都做了博文说明,无奈个人觉得理解不够深入,所以有了这篇更加深入的介绍,希望各位看官拍砖~

二、从是什么开始

1、首先我们得知道 CoordinatorLayout 是什么玩意儿,到底有什么用,我们不妨看看官方文档的描述:   

CoordinatorLayout 是一个 “加强版” FrameLayout, 它主要有两个用途:
1) 用作应用的顶层布局管理器,也就是作为用户界面中所有 UI 控件的容器;
2) 用作相互之间具有特定交互行为的 UI 控件的容器,通过为 CoordinatorLayout 的子 View 指定 Behavior, 就可以实现它们之间的交互行为。 Behavior 可以用来实现一系列的交互行为和布局变化,比如说侧滑菜单、可滑动删除的 UI 元素,以及跟随着其他 UI 控件移动的按钮等。

其实总结出来就是 CoordinatorLayout 是一个布局管理器,相当于一个增强版的 FrameLayout,但是它神奇在于可以实现它的子 View 之间的交互行为。

2、交互行为?
先看个简单的效果图

可能大家看到这,就自然能想到观察者模式,或者我前面写的Rx模式:知识必备】RxJava+Retrofit最佳结合体验,打造懒人封装框架~

我们的 Button 就是一个被观察者,TextView 作为一个观察者,当 Button 移动的时候通知 TextViewTextView 就跟着移动。看看其布局:  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.nanchen.coordinatorlayoutdemo.CoordinatorActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="观察者"
app:layout_behavior=".FollowBehavior"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="被观察者"
android:layout_gravity="center"
android:id="@+id/btn"/>
</android.support.design.widget.CoordinatorLayout>

很简单,一个 TextView, 一个 Button, 外层用 CoordinatorLayout, 然后给我们的 TextView 加一个自定义的 Behavior 文件,内容如下:  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.nanchen.coordinatorlayoutdemo;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
/**
*
* 自定义 CoordinatorLayout 的 Behavior, 泛型为观察者 View ( 要跟着别人动的那个 )
*
* 必须重写两个方法,layoutDependOn和onDependentViewChanged
*
* @author nanchen
* @fileName CoordinatorLayoutDemo
* @packageName com.nanchen.coordinatorlayoutdemo
* @date 2016/12/13 10:13
*/
public class FollowBehavior extends CoordinatorLayout.Behavior<TextView>{
/**
* 构造方法
*/
public FollowBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 判断child的布局是否依赖 dependency
*
* 根据逻辑来判断返回值,返回 false 表示不依赖,返回 true 表示依赖
*
* 在一个交互行为中,Dependent View 的变化决定了另一个相关 View 的行为。
* 在这个例子中, Button 就是 Dependent View,因为 TextView 跟随着它。
* 实际上 Dependent View 就相当于我们前面介绍的被观察者
*
*/
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
return dependency instanceof Button;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
child.setX(dependency.getX());
child.setY(dependency.getY() + 100);
return true;
}
}

重点看看其中重写的两个方法 layoutDependsOn()onDependentViewChanged() 。在介绍这两个方法的作用前,我们先来介绍一下 Dependent View。在一个交互行为中,Dependent View 的变化决定了另一个相关 View 的行为。在这个例子中, Button 就是 Dependent View, 因为 TextView 跟随着它。实际上 Dependent View 就相当于我们前面介绍的被观察者。

知道了这个概念,让我们看看重写的两个方法的作用:

  • layoutDependsOn():这个方法在对界面进行布局时至少会调用一次,用来确定本次交互行为中的 Dependent View,在上面的代码中,当 Dependency 是Button 类的实例时返回 true,就可以让系统知道布局文件中的 Button 就是本次交互行为中的 Dependent View。

  • onDependentViewChanged():当 Dependent View 发生变化时,这个方法会被调用,参数中的child相当于本次交互行为中的观察者,观察者可以在这个方法中对被观察者的变化做出响应,从而完成一次交互行为。

所以我们现在可以开始写Activity中的代码: 

1
2
3
4
5
6
7
8
9
10
findViewById(R.id.btn).setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_MOVE){
view.setX(motionEvent.getRawX()-view.getWidth()/2);
view.setY(motionEvent.getRawY()-view.getHeight()/2);
}
return true;
}
});

这样一来,我们就完成了为 TextView 和Button 设置跟随移动这个交互行为。很简单有木有,其实为 CoordinatorLayout 的子 View 设置交互行为只需三步:
自定义一个继承自 Behavior 类的交互行为类;

把观察者的 layout_behavior 属性设置为自定义行为类的类名;

重写 Behavior 类的相关方法来实现我们想要的交互行为。

值得注意的是,有些时候,并不需要我们自己来定义一个 Behavior 类,因为系统为我们预定义了不少 Behavior 类。在接下来的篇章中,我们会做出进一步的介绍。

3、更进一步
现在我们已经知道了怎么通过给 CoordinatorLayout 的子 View 设置 Behavior 来实现交互行为。现在,让我们更进一步地挖掘下 CoordinatorLayout, 深入了解一下隐藏在表象背后的神秘细节。
实际上, CoordinatorLayout 本身并没有做过多工作,实现交互行为的主要幕后推手是 CoordinatorLayout 的内部类—— Behavior。通过为 CoordinatorLayout直接子 View 绑定一个 Behavior ,这个 Behavior 就会拦截发生在这个 View 上的 Touch 事件、嵌套滚动等。不仅如此,Behavior 还能拦截对与它绑定的 View 的测量及布局。关于嵌套滚动,我们会在后续文章中进行详细介绍。下面我们来深入了解一下 Behavior 是如何做到这一切的。

4、深入理解 Behavior

  • 拦截 Touch 事件

当我们为一个 CoordinatorLayout 的直接子 View 设置了 Behavior 时,这个 Behavior 就能拦截发生在这个 View 上的 Touch 事件,那么它是如何做到的呢?实际上, CoordinatorLayout 重写了 onInterceptTouchEvent() 方法,并在其中给 Behavior 开了个后门,让它能够先于 View 本身处理 Touch 事件。具体来说, CoordinatorLayoutonInterceptTouchEvent() 方法中会遍历所有直接子 View ,对于绑定了 Behavior 的直接子 View 调用 Behavior 的 onInterceptTouchEvent() 方法,若这个方法返回 true, 那么后续本该由相应子 View 处理的 Touch 事件都会交由 Behavior 处理,而 View 本身表示懵逼,完全不知道发生了什么。

  • 拦截测量及布局

了解了 Behavior 是怎养拦截 Touch 事件的,想必大家已经猜出来了它拦截测量及布局事件的方式 —— CoordinatorLayout 重写了测量及布局相关的方法并为 Behavior 开了个后门。没错,真相就是如此。
CoordinatorLayout 在 onMeasure() 方法中,会遍历所有直接子 View ,若该子 View 绑定了一个 Behavior ,就会调用相应 Behavior 的 onMeasureChild() 方法,若此方法返回 true,那么 CoordinatorLayout 对该子 View 的测量就不会进行。这样一来, Behavior 就成功接管了对 View 的测量。
同样,CoordinatorLayout 在 onLayout() 方法中也做了与 onMeasure() 方法中相似的事,让 Behavior 能够接管对相关子 View 的布局。

  • View 的依赖关系的确定
    现在,我们在探究一下交互行为中的两个 View 之间的依赖关系是怎么确定的。我们称 child 为交互行为中根据另一个 View 的变化做出响应的那个个体,而 Dependent View 为child所依赖的 View。实际上,确立 child 和 Dependent View 的依赖关系有两种方式:

1) 显式依赖:为 child 绑定一个 Behavior,并在 Behavior 类的 layoutDependsOn() 方法中做手脚。即当传入的 dependency 为 Dependent View 时返回 true,这样就建立了 child 和 Dependent View 之间的依赖关系。

2) 隐式依赖:通过我们最开始提到的锚(anchor)来确立。具体做法可以这样:在 XML 布局文件中,把 child 的 layout_anchor 属性设为 Dependent View 的id,然后 child 的 layout_anchorGravity 属性用来描述为它想对 Dependent View 的变化做出什么样的响应。关于这个我们会在后续篇章给出具体示例。

无论是隐式依赖还是显式依赖,在 Dependent View 发生变化时,相应 Behavior 类的 onDependentViewChanged() 方法都会被调用,在这个方法中,我们可以让 child 做出改变以响应 Dependent View 的变化。

三、玩转AppBarLayout

实际上我们在应用中有 CoordinatorLayout 的地方通常都会有 AppBarLayout 的联用,作为同样的出自 Design 包的库,我们看看官方文档怎么说:

AppBarLayout 是一个垂直的 LinearLayout,实现了 Material Design 中 App bar 的 Scrolling Gestures 特性。AppBarLayout 的子 View 应该声明想要具有的“滚动行为”,这可以通过 layout_scrollFlags 属性或是 setScrollFlags() 方法来指定。

AppBarLayout 只有作为 CoordinatorLayout 的直接子 View 时才能正常工作,为了让 AppBarLayout 能够知道何时滚动其子 View,我们还应该在 CoordinatorLayout 布局中提供一个可滚动 View,我们称之为 Scrolling View。

Scrolling View 和 AppBarLayout 之间的关联,通过将 Scrolling View 的 Behavior 设为 AppBarLayout.ScrollingViewBehavior 来建立。

1、一般怎么用?
AppBar 是 Design 的一个概念,其实我们也可以把它看做一种 5.0 出的 ToolBar,先感受一下 AppBarLayout + CoordinatorLayout 的魅力。

实际效果就是这样,当向上滑动 View 的时候,ToolBar 会小时,向下滑动的时候,ToolBar 又会出现,但别忘了,这是 AppBarLayout 的功能,ToolBar 可办不到。由于要滑动,那么我们的 AppBarLayout 一定是和可以滑动的 View 一起使用的,比如 RecyclerViewScollView 等。
我们看看上面的到底怎么实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_coor_app_bar"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.nanchen.coordinatorlayoutdemo.CoorAppBarActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/recycler"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>

我们可以看到,上面出现了一个 app:layouy_scrollFrags 的自定义属性设置,这个属性可以定义我们不同的滚动行为。

2、layout_scrollFlags
根据官方文档,layout_scrollFlags 的取值可以为以下几种。

  • scroll
    设成这个值的效果就好比本 View 和 Scrolling view 是“一体”的。具体示例我们在上面已经给出。有一点特别需要我们的注意,为了其他的滚动行为生效,必须同时指定 Scroll 和相应的标记,比如我们想要 exitUntilCollapsed 所表现的滚动行为,必须将 layout_scrollFlags 指定为 scroll|exitUntilCollapsed

  • exitUntilCollapsed
    当本 View 离开屏幕时,会被“折叠”直到达到其最小高度。我们可以这样理解这个效果:当我们开始向上滚动 Scrolling view 时,本 View 会先接管滚动事件,这样本 View 会先进行滚动,直到滚动到了最小高度(折叠了),Scrolling view 才开始实际滚动。而当本 View 已完全折叠后,再向下滚动 Scrolling view,直到 Scrolling view 顶部的内容完全显示后,本 View 才会开始向下滚动以显现出来。

  • enterAlways
    当 Scrolling view 向下滚动时,本 View 会一起跟着向下滚动。实际上就好比我们同时对 Scrolling view 和本 View 进行向下滚动。

  • enterAlwaysCollapsed
    从名字上就可以看出,这是在 enterAlways 的基础上,加上了“折叠”的效果。当我们开始向下滚动 Scrolling View 时,本 View 会一起跟着滚动直到达到其“折叠高度”(即最小高度)。然后当 Scrolling View 滚动至顶部内容完全显示后,再向下滚动 Scrolling View,本 View 会继续滚动到完全显示出来。  

  • snap
    在一次滚动结束时,本 View 很可能只处于“部分显示”的状态,加上这个标记能够达到“要么完全隐藏,要么完全显示”的效果。

四、CollapsingToolBarLayout

这个东西,我相信很多博客和技术文章都会把 CollapsingToolBarLayoutCoordinatorLayout 放一起讲,这个东西的确很牛。我们同样先看看官方文档介绍:

CollapsingToolbarLayout 通常用来在布局中包裹一个 Toolbar,以实现具有“折叠效果“”的顶部栏。它需要是 AppBarLayout 的直接子 View,这样才能发挥出效果。

CollapsingToolbarLayout包含以下特性:

  • Collasping title(可折叠标题):当布局完全可见时,这个标题比较大;当折叠起来时,标题也会变小。标题的外观可以通过 expandedTextAppearance 和 collapsedTextAppearance 属性来调整。
  • Content scrim(内容纱布):根据 CollapsingToolbarLayout 是否滚动到一个临界点,内容纱布会显示或隐藏。可以通过 setContentScrim(Drawable) 来设置内容纱布。
  • Status bar scrim(状态栏纱布):也是根据是否滚动到临界点,来决定是否显示。可以通过 setStatusBarScrim(Drawable) 方法来设置。这个特性只有在 Android 5.0 及其以上版本,我们设置 fitSystemWindows 为 ture 时才能生效。
  • Parallax scrolling children(视差滚动子 View):子 View 可以选择以“视差”的方式来进行滚动。(视觉效果上就是子 View 滚动的比其他 View 稍微慢些)
  • Pinned position children:子 View 可以选择固定在某一位置上。

上面的描述有些抽象,实际上对于 Content scrimStatus bar scrim 我们可以暂时予以忽略,只要留个大概印象待以后需要时再查阅相关资料即可。下面我们通过一个常见的例子介绍下 CollapsingToolbarLayout 的基本使用姿势。
我们来看看一个常用的效果:

看看布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_coor_tool_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.nanchen.coordinatorlayoutdemo.CoorToolBarActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:theme="@style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="200dp"
app:contentScrim="@color/colorPrimary"
app:expandedTitleMarginStart="100dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:statusBarScrim="@android:color/transparent"
app:titleEnabled="false">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
android:src="@mipmap/logo"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.6"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title=""/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/recycler"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<TextView
android:id="@+id/toolbar_title"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_marginLeft="16dp"
android:layout_marginTop="-100dp"
android:alpha="0"
android:elevation="10dp"
android:gravity="center_vertical"
android:text="爱吖校推-你关注的,我们才推"
android:textColor="@android:color/white"
android:textSize="20sp"
android:textStyle="bold"
app:layout_behavior=".SimpleViewBehavior"
app:svb_dependOn="@id/appbar"
app:svb_dependType="y"
app:svb_targetAlpha="1"
app:svb_targetY="0dp"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@mipmap/ic_start"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|right"/>
</android.support.design.widget.CoordinatorLayout>

我们在 XML 文件中为 CollapsingToolBarLayout 的 layout_scrollFlags 指定为 scroll|exitUntilCollapsed|snap,这样便实现了向上滚动的折叠效果。

CollapsingToolbarLayout 本质上同样是一个 FrameLayout,我们在布局文件中指定了一个 ImageView 和一个 ToolbarImageViewlayout_collapseMode 属性设为了 parallax,也就是我们前面介绍的视差滚动;而 Toolbar 的 layout_collaspeMode 设为了 pin ,也就是 Toolbar 会始终固定在顶部。

五、写在最后

本次的 Design 包下的 CoordinatorLayoutAppBarLayout 就讲述到这里,后续还将持续更新,欢迎拍砖~
查看源码请移步 Github:https://github.com/nanchen2251/CoordinatorAppBarDemo

欢迎关注我的公众号 ( nanchen ) ,投稿被选中有惊喜!