轉載自:http://www.jianshu.com/p/640f4ef05fb2
一、寫在前面
其實博主在之前已經對 Design 包的各個控件都做了博文說明,無奈個人覺得理解不夠深入,所以有了這篇更加深入的介紹,希望各位看官拍磚~
二、從是什麼開始
1、首先我們得知道 CoordinatorLayout
是什麼玩意兒,到底有什麼用,我們不妨看看官方文檔的描述:
CoordinatorLayout
是一個 “加強版”FrameLayout
, 它主要有兩個用途:
1) 用作應用的頂層佈局管理器,也就是作爲用戶界面中所有 UI 控件的容器;
2) 用作相互之間具有特定交互行爲的 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 的依賴關係有兩種方式:
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 一起使用的,比如
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 會始終固定在頂部。