一文徹底搞懂 Design 設計的 CoordinatorLayout 和 AppbarLayout 聯動,讓 Design 設計更簡單~

轉載自: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 移動的時候通知 TextViewTextView 就跟着移動。看看其佈局:  

<?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 事件。具體來說, 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 等。
我們看看上面的到底怎麼實現的:

<?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 的基本使用姿勢。
我們來看看一個常用的效果:



看看佈局:

<?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 會始終固定在頂部。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章