一、写在前面
其实博主在之前已经对 Design 包的各个控件都做了博文说明,无奈个人觉得理解不够深入,所以有了这篇更加深入的介绍,希望各位看官拍砖~
二、从是什么开始
1、首先我们得知道 CoordinatorLayout
是什么玩意儿,到底有什么用,我们不妨看看官方文档的描述:
CoordinatorLayout
是一个 “加强版”FrameLayout
, 它主要有两个用途:
- 用作应用的顶层布局管理器,也就是作为用户界面中所有 UI 控件的容器;
- 用作相互之间具有特定交互行为的 UI 控件的容器,通过为
CoordinatorLayout
的子 View 指定 Behavior, 就可以实现它们之间的交互行为。 Behavior 可以用来实现一系列的交互行为和布局变化,比如说侧滑菜单、可滑动删除的 UI 元素,以及跟随着其他 UI 控件移动的按钮等。
其实总结出来就是 CoordinatorLayout
是一个布局管理器,相当于一个增强版的 FrameLayout
,但是它神奇在于可以实现它的子 View 之间的交互行为。
2、交互行为?
先看个简单的效果图
可能大家看到这,就自然能想到观察者模式,或者我前面写的Rx模式:这可能是最好的RxJava 2.x 教程(完结版)
我们的 Button
就是一个被观察者,TextView 作为一个观察者,当 Button
移动的时候通知 TextView
, TextView
就跟着移动。看看其布局:
<?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
文件,内容如下:
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中的代码:
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 事件。具体来说, CoordinatorLayout
的 onInterceptTouchEvent()
方法中会遍历所有直接子 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 的依赖关系有两种方式:
显式依赖:为 child 绑定一个 Behavior,并在 Behavior 类的
layoutDependsOn()
方法中做手脚。即当传入的dependency
为 Dependent View 时返回 true,这样就建立了 child 和 Dependent View 之间的依赖关系。隐式依赖:通过我们最开始提到的锚(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 一起使用的,比如
RecyclerView
,ScollView
等。我们看看上面的到底怎么实现的:
<?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
这个东西,我相信很多博客和技术文章都会把 CollapsingToolBarLayout
和 CoordinatorLayout
放一起讲,这个东西的确很牛。我们同样先看看官方文档介绍:
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 scrim
、Status bar scrim
我们可以暂时予以忽略,只要留个大概印象待以后需要时再查阅相关资料即可。下面我们通过一个常见的例子介绍下 CollapsingToolbarLayout
的基本使用姿势。
我们来看看一个常用的效果:
看看布局:
<?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
和一个 Toolbar
。ImageView
的layout_collapseMode
属性设为了 parallax
,也就是我们前面介绍的视差滚动;而 Toolbar 的 layout_collaspeMode
设为了 pin
,也就是 Toolbar 会始终固定在顶部。
五、写在最后
本次的 Design 包下的 CoordinatorLayout
和 AppBarLayout
就讲述到这里,后续还将持续更新,欢迎拍砖~
查看源码请移步 Github:https://github.com/nanchen2251/CoordinatorAppBarDemo