一、寫在前面
其實博主在之前已經(jīng)對 Design 包的各個控件都做了博文說明,無奈個人覺得理解不夠深入,所以有了這篇更加深入的介紹,希望各位看官拍磚~
二、從是什么開始
1、首先我們得知道 CoordinatorLayout 是什么玩意兒,到底有什么用,我們不妨看看官方文檔的描述:
CoordinatorLayout是一個 “加強版”FrameLayout, 它主要有兩個用途:
- 用作應(yīng)用的頂層布局管理器,也就是作為用戶界面中所有 UI 控件的容器;
- 用作相互之間具有特定交互行為的 UI 控件的容器,通過為
CoordinatorLayout的子 View 指定 Behavior, 就可以實現(xiàn)它們之間的交互行為。 Behavior 可以用來實現(xiàn)一系列的交互行為和布局變化,比如說側(cè)滑菜單、可滑動刪除的 UI 元素,以及跟隨著其他 UI 控件移動的按鈕等。
其實總結(jié)出來就是 CoordinatorLayout 是一個布局管理器,相當(dāng)于一個增強版的 FrameLayout,但是它神奇在于可以實現(xiàn)它的子 View 之間的交互行為。
2、交互行為?
先看個簡單的效果圖

可能大家看到這,就自然能想到觀察者模式,或者我前面寫的Rx模式:這可能是最好的RxJava 2.x 教程(完結(jié)版)
我們的 Button 就是一個被觀察者,TextView 作為一個觀察者,當(dāng) Button 移動的時候通知 TextView, TextView 就跟著移動??纯雌洳季郑?/p>
<?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 文件,內(nèi)容如下:
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>{
/**
* 構(gòu)造方法
*/
public FollowBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 判斷child的布局是否依賴 dependency
*
* 根據(jù)邏輯來判斷返回值,返回 false 表示不依賴,返回 true 表示依賴
*
* 在一個交互行為中,Dependent View 的變化決定了另一個相關(guān) View 的行為。
* 在這個例子中, Button 就是 Dependent View,因為 TextView 跟隨著它。
* 實際上 Dependent View 就相當(dāng)于我們前面介紹的被觀察者
*
*/
@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 的變化決定了另一個相關(guān) View 的行為。在這個例子中, Button 就是 Dependent View, 因為 TextView 跟隨著它。實際上 Dependent View 就相當(dāng)于我們前面介紹的被觀察者。
知道了這個概念,讓我們看看重寫的兩個方法的作用:
layoutDependsOn():這個方法在對界面進(jìn)行布局時至少會調(diào)用一次,用來確定本次交互行為中的 Dependent View,在上面的代碼中,當(dāng)Dependency是Button 類的實例時返回 true,就可以讓系統(tǒng)知道布局文件中的 Button 就是本次交互行為中的 Dependent View。onDependentViewChanged():當(dāng) Dependent View 發(fā)生變化時,這個方法會被調(diào)用,參數(shù)中的child相當(dāng)于本次交互行為中的觀察者,觀察者可以在這個方法中對被觀察者的變化做出響應(yīng),從而完成一次交互行為。
所以我們現(xiàn)在可以開始寫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 設(shè)置跟隨移動這個交互行為。很簡單有木有,其實為 CoordinatorLayout 的子 View 設(shè)置交互行為只需三步:
自定義一個繼承自 Behavior 類的交互行為類;
把觀察者的 layout_behavior 屬性設(shè)置為自定義行為類的類名;
重寫 Behavior 類的相關(guān)方法來實現(xiàn)我們想要的交互行為。
值得注意的是,有些時候,并不需要我們自己來定義一個 Behavior 類,因為系統(tǒng)為我們預(yù)定義了不少 Behavior 類。在接下來的篇章中,我們會做出進(jìn)一步的介紹。
3、更進(jìn)一步
現(xiàn)在我們已經(jīng)知道了怎么通過給 CoordinatorLayout 的子 View 設(shè)置 Behavior 來實現(xiàn)交互行為。現(xiàn)在,讓我們更進(jìn)一步地挖掘下 CoordinatorLayout, 深入了解一下隱藏在表象背后的神秘細(xì)節(jié)。
實際上, CoordinatorLayout 本身并沒有做過多工作,實現(xiàn)交互行為的主要幕后推手是 CoordinatorLayout 的內(nèi)部類—— Behavior。通過為 CoordinatorLayout 的**直接子 View **綁定一個 Behavior ,這個 Behavior 就會攔截發(fā)生在這個 View 上的 Touch 事件、嵌套滾動等。不僅如此,Behavior 還能攔截對與它綁定的 View 的測量及布局。關(guān)于嵌套滾動,我們會在后續(xù)文章中進(jìn)行詳細(xì)介紹。下面我們來深入了解一下 Behavior 是如何做到這一切的。
4、深入理解 Behavior
- 攔截 Touch 事件
當(dāng)我們?yōu)橐粋€ CoordinatorLayout 的直接子 View 設(shè)置了 Behavior 時,這個 Behavior 就能攔截發(fā)生在這個 View 上的 Touch 事件,那么它是如何做到的呢?實際上, CoordinatorLayout 重寫了 onInterceptTouchEvent() 方法,并在其中給 Behavior 開了個后門,讓它能夠先于 View 本身處理 Touch 事件。具體來說, CoordinatorLayout 的 onInterceptTouchEvent() 方法中會遍歷所有直接子 View ,對于綁定了 Behavior 的直接子 View 調(diào)用 Behavior 的 onInterceptTouchEvent() 方法,若這個方法返回 true, 那么后續(xù)本該由相應(yīng)子 View 處理的 Touch 事件都會交由 Behavior 處理,而 View 本身表示懵逼,完全不知道發(fā)生了什么。
- 攔截測量及布局
了解了 Behavior 是怎養(yǎng)攔截 Touch 事件的,想必大家已經(jīng)猜出來了它攔截測量及布局事件的方式 —— CoordinatorLayout 重寫了測量及布局相關(guān)的方法并為 Behavior 開了個后門。沒錯,真相就是如此。
CoordinatorLayout 在 onMeasure() 方法中,會遍歷所有直接子 View ,若該子 View 綁定了一個 Behavior ,就會調(diào)用相應(yīng) Behavior 的 onMeasureChild() 方法,若此方法返回 true,那么 CoordinatorLayout 對該子 View 的測量就不會進(jìn)行。這樣一來, Behavior 就成功接管了對 View 的測量。
同樣,CoordinatorLayout 在 onLayout() 方法中也做了與 onMeasure() 方法中相似的事,讓 Behavior 能夠接管對相關(guān)子 View 的布局。
- View 的依賴關(guān)系的確定
現(xiàn)在,我們在探究一下交互行為中的兩個 View 之間的依賴關(guān)系是怎么確定的。我們稱 child 為交互行為中根據(jù)另一個 View 的變化做出響應(yīng)的那個個體,而 Dependent View 為child所依賴的 View。實際上,確立 child 和 Dependent View 的依賴關(guān)系有兩種方式:
顯式依賴:為 child 綁定一個 Behavior,并在 Behavior 類的
layoutDependsOn()方法中做手腳。即當(dāng)傳入的dependency為 Dependent View 時返回 true,這樣就建立了 child 和 Dependent View 之間的依賴關(guān)系。隱式依賴:通過我們最開始提到的錨(anchor)來確立。具體做法可以這樣:在 XML 布局文件中,把 child 的
layout_anchor屬性設(shè)為 Dependent View 的id,然后 child 的layout_anchorGravity屬性用來描述為它想對 Dependent View 的變化做出什么樣的響應(yīng)。關(guān)于這個我們會在后續(xù)篇章給出具體示例。
無論是隱式依賴還是顯式依賴,在 Dependent View 發(fā)生變化時,相應(yīng) Behavior 類的 onDependentViewChanged() 方法都會被調(diào)用,在這個方法中,我們可以讓 child 做出改變以響應(yīng) Dependent View 的變化。
三、玩轉(zhuǎn)AppBarLayout
實際上我們在應(yīng)用中有 CoordinatorLayout 的地方通常都會有 AppBarLayout 的聯(lián)用,作為同樣的出自 Design 包的庫,我們看看官方文檔怎么說:
AppBarLayout 是一個垂直的 LinearLayout,實現(xiàn)了 Material Design 中 App bar 的 Scrolling Gestures 特性。AppBarLayout 的子 View 應(yīng)該聲明想要具有的“滾動行為”,這可以通過 layout_scrollFlags 屬性或是 setScrollFlags() 方法來指定。
AppBarLayout 只有作為 CoordinatorLayout 的直接子 View 時才能正常工作,為了讓 AppBarLayout 能夠知道何時滾動其子 View,我們還應(yīng)該在 CoordinatorLayout 布局中提供一個可滾動 View,我們稱之為 Scrolling View。
Scrolling View 和 AppBarLayout 之間的關(guān)聯(lián),通過將 Scrolling View 的 Behavior 設(shè)為 AppBarLayout.ScrollingViewBehavior 來建立。
1、一般怎么用?
AppBar 是 Design 的一個概念,其實我們也可以把它看做一種 5.0 出的 ToolBar,先感受一下 AppBarLayout + CoordinatorLayout 的魅力。

實際效果就是這樣,當(dāng)向上滑動 View 的時候,ToolBar 會小時,向下滑動的時候,ToolBar 又會出現(xiàn),但別忘了,這是 AppBarLayout 的功能,ToolBar 可辦不到。由于要滑動,那么我們的 AppBarLayout 一定是和可以滑動的 View 一起使用的,比如
RecyclerView,ScollView 等。我們看看上面的到底怎么實現(xiàn)的:
<?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>
我們可以看到,上面出現(xiàn)了一個 app:layouy_scrollFrags 的自定義屬性設(shè)置,這個屬性可以定義我們不同的滾動行為。
**2、layout_scrollFlags **
根據(jù)官方文檔,layout_scrollFlags 的取值可以為以下幾種。
scroll
設(shè)成這個值的效果就好比本 View 和 Scrolling view 是“一體”的。具體示例我們在上面已經(jīng)給出。有一點特別需要我們的注意,為了其他的滾動行為生效,必須同時指定 Scroll 和相應(yīng)的標(biāo)記,比如我們想要exitUntilCollapsed所表現(xiàn)的滾動行為,必須將 layout_scrollFlags 指定為scroll|exitUntilCollapsed。exitUntilCollapsed
當(dāng)本 View 離開屏幕時,會被“折疊”直到達(dá)到其最小高度。我們可以這樣理解這個效果:當(dāng)我們開始向上滾動 Scrolling view 時,本 View 會先接管滾動事件,這樣本 View 會先進(jìn)行滾動,直到滾動到了最小高度(折疊了),Scrolling view 才開始實際滾動。而當(dāng)本 View 已完全折疊后,再向下滾動 Scrolling view,直到 Scrolling view 頂部的內(nèi)容完全顯示后,本 View 才會開始向下滾動以顯現(xiàn)出來。enterAlways
當(dāng) Scrolling view 向下滾動時,本 View 會一起跟著向下滾動。實際上就好比我們同時對 Scrolling view 和本 View 進(jìn)行向下滾動。enterAlwaysCollapsed
從名字上就可以看出,這是在enterAlways的基礎(chǔ)上,加上了“折疊”的效果。當(dāng)我們開始向下滾動 Scrolling View 時,本 View 會一起跟著滾動直到達(dá)到其“折疊高度”(即最小高度)。然后當(dāng) Scrolling View 滾動至頂部內(nèi)容完全顯示后,再向下滾動 Scrolling View,本 View 會繼續(xù)滾動到完全顯示出來。snap
在一次滾動結(jié)束時,本 View 很可能只處于“部分顯示”的狀態(tài),加上這個標(biāo)記能夠達(dá)到“要么完全隱藏,要么完全顯示”的效果。
四、CollapsingToolBarLayout
這個東西,我相信很多博客和技術(shù)文章都會把 CollapsingToolBarLayout 和 CoordinatorLayout 放一起講,這個東西的確很牛。我們同樣先看看官方文檔介紹:
CollapsingToolbarLayout 通常用來在布局中包裹一個 Toolbar,以實現(xiàn)具有“折疊效果“”的頂部欄。它需要是 AppBarLayout 的直接子 View,這樣才能發(fā)揮出效果。
CollapsingToolbarLayout包含以下特性:
- Collasping title(可折疊標(biāo)題):當(dāng)布局完全可見時,這個標(biāo)題比較大;當(dāng)折疊起來時,標(biāo)題也會變小。標(biāo)題的外觀可以通過 expandedTextAppearance 和 collapsedTextAppearance 屬性來調(diào)整。
- Content scrim(內(nèi)容紗布):根據(jù) CollapsingToolbarLayout 是否滾動到一個臨界點,內(nèi)容紗布會顯示或隱藏??梢酝ㄟ^ setContentScrim(Drawable) 來設(shè)置內(nèi)容紗布。
- Status bar scrim(狀態(tài)欄紗布):也是根據(jù)是否滾動到臨界點,來決定是否顯示??梢酝ㄟ^ setStatusBarScrim(Drawable) 方法來設(shè)置。這個特性只有在 Android 5.0 及其以上版本,我們設(shè)置 fitSystemWindows 為 ture 時才能生效。
- Parallax scrolling children(視差滾動子 View):子 View 可以選擇以“視差”的方式來進(jìn)行滾動。(視覺效果上就是子 View 滾動的比其他 View 稍微慢些)
- Pinned position children:子 View 可以選擇固定在某一位置上。
上面的描述有些抽象,實際上對于 Content scrim 、Status bar scrim 我們可以暫時予以忽略,只要留個大概印象待以后需要時再查閱相關(guān)資料即可。下面我們通過一個常見的例子介紹下 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="愛吖校推-你關(guān)注的,我們才推"
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,這樣便實現(xiàn)了向上滾動的折疊效果。
CollapsingToolbarLayout 本質(zhì)上同樣是一個 FrameLayout,我們在布局文件中指定了一個 ImageView 和一個 Toolbar。ImageView 的layout_collapseMode 屬性設(shè)為了 parallax,也就是我們前面介紹的視差滾動;而 Toolbar 的 layout_collaspeMode 設(shè)為了 pin ,也就是 Toolbar 會始終固定在頂部。
五、寫在最后
本次的 Design 包下的 CoordinatorLayout 和 AppBarLayout 就講述到這里,后續(xù)還將持續(xù)更新,歡迎拍磚~
查看源碼請移步 Github:https://github.com/nanchen2251/CoordinatorAppBarDemo