玩轉CoordinatorLayout

作爲Material Design風格的重要組件,CoordinatorLayout協調多種組件的聯動,實現各種複雜的效果,在實際項目中扮演着越來越重要的角色。本篇博客將由淺到深,帶你一起玩轉CoordinatorLayout。

官方文檔對CoordinatorLayout是這樣描述的:

這裏寫圖片描述

CoordinatorLayout是一個加強版的FrameLayout,本質是一個ViewGroup,主要有兩個用途:
1.用作應用的頂層佈局管理器,作爲界面其他控件的父容器
2.用作相互之間有特定交互行爲的控件的父容器
通過爲CoordinatorLayout的子View指定不同的Behavior(默認的Behavior或自定義的Behavior),就可以實現它們之間許多複雜的交互行爲,例如側滑,移動,滑動等。Behavior在後面會詳談,最後我們再來瞅一眼源碼:

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {}

先來了解一下NestedScrolling—Android嵌套滑動機制,用來完成複雜的滑動效果。要完成這樣的交互,父View需要實現 NestedScrollingParent 接口,而子View需要實現 NestedScrollingChild 接口。CoordinatorLayout其實是NestedScrollingParent的實現類,也就意味着它的子View一定會實現NestedScrollingChild接口,這樣一起協同完成複雜的滑動交互。

介紹工作就到這裏,接下來通過三個例子,一起學習CoordinatorLayout(協調佈局)。

一.CoordinatorLayout與AppBarLayout

官方文檔中對AppBarLayout是這樣描述的:AppBarLayout是一個垂直的LinearLayout,只有作爲CoordinatorLayout的直接子View時才能正常工作,可以通過設置layout_scrollFlags屬性或setScrollFlags()方法讓AppBarLayout的子View具有“滾動行爲”。我們先來看一個例子,再來分析這段描述。

這裏寫圖片描述

列表向上滑動,佈局收縮;列表向下滑動,佈局向下展開到一定高度,待列表完全向下滑動到起始位置,佈局再次向下展開。看看怎麼實現的:

佈局代碼:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:background="@color/colorPrimary"
                android:minHeight="50dp"
               app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"></LinearLayout>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"></android.support.v7.widget.Toolbar>

        </android.support.design.widget.AppBarLayout>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"></android.support.v7.widget.RecyclerView>

    </android.support.design.widget.CoordinatorLayout>

</LinearLayout>

CoordinatorLayout中,AppBarLayout與RecyclerView豎直排列;AppBarLayout中,LinearLayout與Toolbar豎直排列。AppBarLayout的官方文檔告訴我們,通過設置layout_scrollFlags屬性可以讓子View具有滑動行爲。結合佈局文件可以看到,我們給LinearLayout設置了三個屬性,都代表什麼意思呢:

scroll:設成這個值的效果就是該View和scrolling view形成一個整體,示例中我們滑動RecyclerView時,LinearLayout也會響應滑動。有一點特別需要我們的注意,爲了其他的滾動行爲生效,必須同時指定scroll和其他標記。
exitUntilCollapsed:設成這個值的效果就是該View離開屏幕時,會被摺疊到最小高度。該View已完全摺疊後,再向下滾動scrolling view,直到scrolling view頂部的內容完全顯示後,該View纔會開始向下滾動以顯現出來。
enterAlways:設成這個值的效果就是當scrolling view向下滾動時,該View會一起跟着向下滾動,示例圖中有所體現。
enterAlwaysCollapsed:設成這個值的效果就是當我們開始向下滾動scrolling view時,該View會一起跟着滾動直到達到其最小高度。然後當scrolling view滾動至頂部內容完全顯示後,再向下滾動scrolling view,該View會繼續滾動到完全顯示出來。示例圖中有所體現。

篇幅有限,這裏就不多做演示了,大家可以自己試試,組合成不同的效果。

通過分析CoordinatorLayout我們知道它的子View一定會實現NestedScrollingChild接口,這樣一起完成複雜的滑動交互,我們看到佈局中有一個RecyclerView,先瞅一眼源碼:

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {}

Bingo!RecyclerView其實就是NestedScrollingChild的實現類,這也就解釋了嵌套滑動的實現。這裏如果使用NestedScrollView,然後在裏面嵌套一個佈局也是可以的。最後我們還爲RecyclerView設置了這樣一個屬性:

 app:layout_behavior="@string/appbar_scrolling_view_behavior">

這是因爲CoordinatorLayout包含的子視圖中,帶有滾動屬性的View必須設置app:layout_behavior屬性,設置的這個屬性到底是什麼呢:

android.support.design.widget.AppBarLayout$ScrollingViewBehavior

原來是AppBarLayout自帶一個Behivior,直接在源碼裏註解聲明的,這個Behivior也只能用於AppBarLayout,作用是讓他根據CoordinatorLayout上的手勢進行一些效果(比如收縮,滾動)。關於CoordinatorLayout與Behivior工作原理,我們後面再談。

二.CoordinatorLayout與CollapsingToolbarLayout
CollapsingToolbarLayout(摺疊佈局)繼承至FrameLayout,通過給它設置layout_scrollFlags,它以控制子View響應layout_behavior事件並作出相應的變化(移除屏幕或固定在屏幕頂端)。我們看一個例子:

這裏寫圖片描述

很常見也很好看的一個界面,看看佈局代碼:

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="250dp">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="?attr/colorPrimary"
            app:expandedTitleMarginEnd="60dp"
            app:expandedTitleMarginStart="50dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="fitXY"
                android:src="@drawable/bg"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.7" />


            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>

結合代碼我們看看CollapsingToolbarLayout的使用方法:

1.AppBarLayout與Toolbar的高度一定要設置具體值,這是最後展開與固定的值
2.CollapsingToolbarLayout的屬性設置:layout_scrollFlags一定是必不可少的,屬性值上文提過,這裏不再重複了;contentScrim設置CollapsingToolbarLayout摺疊後的背景顏色;expandedTitleMarginStart 設置沒有收縮時title向左填充的距離;expandedTitleMarginEnd設置收縮結束時title向左填充的距離
3.ImageView的屬性設置:layout_collapseMode (摺疊模式)一定是必不可少的,一共有兩種模式。pin:設置爲這個模式時,當CollapsingToolbarLayout完全收縮後,Toolbar還可以保留在屏幕上;parallax:設置爲這個模式時,在內容滾動時,CollapsingToolbarLayout中的View(比如ImageView)也可以同時滾動,實現視差滾動效果,通常和視差因子搭配使用;layout_collapseParallaxMultiplier設置視差滾動因子,值爲0~1,值越大視察越大。
4.CollapsingToolbarLayout的摺疊、展開狀態監聽: CollapsingToolbarLayout是通過實現AppBarLayout的OnOffsetChangedListener接口,根據AppBarLayout的偏移來實現子View視差移動和顯示。通過調用AppBarLayout的addOnOffsetChangedListener方法監聽AppBarLayout的位移,從而判斷CollapsingToolbarLayout的狀態。

三.CoordinatorLayout與Behavior

前面提到,CoordinatorLayout的子View之間許多複雜的交互行爲是通過指定Behavior實現的。那麼實現原理是什麼,我們看個例子:

這裏寫圖片描述

TextView的位置會隨着Button的改變而改變,並且顯示當前的座標。其實Behavior的原理就是觀察者模式的應用,被觀察者就是事件源dependency,觀察者就是做出改變的child。具體看看怎麼實現的:

public class FollowBehavior extends CoordinatorLayout.Behavior<TextView> {

    public FollowBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @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() + 200);
        child.setY(dependency.getY() + 200);
        child.setText("觀察者:" + dependency.getX() + "," + dependency.getY());
        return true;
    }
}

1.自定義一個FollowBehavior繼承自Behavior,泛型就是child的類型,也就是觀察者View
2.重寫構造函數,因爲CoordinatorLayout源碼中會通過反射調用這個構造函數
3.重寫layoutDependsOn():用來確定本次交互行爲中的dependent view,在上面的代碼中,當dependency是Button類的實例時返回true,就可以讓系統知道佈局文件中的Button就是本次交互行爲中的被觀察者。
4.重寫onDependentViewChanged:當dependent view發生變化時,這個方法會被調用,參數中的child相當於本次交互行爲中的觀察者,觀察者可以在這個方法中對被觀察者的變化做出響應,從而完成一次交互行爲。

代碼佈局中的應用:

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_behavior=".FollowBehavior" />

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="被觀察者" />


</android.support.design.widget.CoordinatorLayout>


        btn.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_MOVE) {
                    v.setX(event.getRawX() - v.getWidth() / 2);
                    v.setY(event.getRawY() - v.getHeight() / 2 - getStatusBarHeight(getApplicationContext()));
                }
                return true;
            }
        });

xml佈局文件中,只需指定

app:layout_behavior=“你的Behavior包含包名的類名”

CoordinatorLayout就會反射生成你的Behavior,這樣自定義的Behavior就能工作了。在Activity代碼中,添加OnTouchListener,獲取觸摸點座標,這裏豎直方向上減去了狀態欄的高度。

既然熟悉了Behavior的原理,這裏我在之前的例子上升級了一下。先看效果圖:

這裏寫圖片描述

摺疊佈局中加了一個圓形頭像,並且圓形頭像的大小,位置會隨着列表上下滑動而改變。沒有美工設計,交互是自己瞎想的,大家湊合看看。重點是掌握自定義Behavior。

這裏的圓形ImageView就是我們的觀察者,給它指定Behavior,從而響應手勢事件。誰是被觀察者呢?這裏我選擇的是AppBarLayout,因爲上文提到過,可以通過AppBarLayout的addOnOffsetChangedListener方法監聽AppBarLayout的位移,從而判斷CollapsingToolbarLayout的狀態。既然能夠獲取到這個位移,那就可以讓觀察者對被觀察者的位移變化做出響應。看看具體實現:

public class FollowBehavior extends CoordinatorLayout.Behavior<ImageView> {

    private int width, height, top, left;

    public FollowBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, ImageView child, View dependency) {
        return dependency instanceof AppBarLayout;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, ImageView child, View dependency) {


        if (dependency.getY() == 0) {
            width = child.getWidth();
            height = child.getHeight();
            top = child.getTop();
            left = child.getLeft();
        }
        float percent = Math.abs(dependency.getY()) / SixActivity.scrollRange;
        float yPercent = (float) (percent * 0.85);
        child.setY(top * (1 - yPercent));
        child.setX(left + 300 * percent);
        CoordinatorLayout.LayoutParams layoutParams =
                (CoordinatorLayout.LayoutParams) child.getLayoutParams();
        layoutParams.width = (int) (width * (1 - percent * 3 / 4));
        layoutParams.height = (int) (height * (1 - percent * 3 / 4));
        child.setLayoutParams(layoutParams);
        return true;
    }
}

具體分析一下onDependentViewChanged這個方法,AppBarLayout的摺疊範圍scrollRange是通過 appBarLayout.getTotalScrollRange()獲取到的,這裏我設置成了靜態常量;當AppBarLayout完全展開,沒有摺疊時,獲取到ImageView的基本屬性;當AppBarLayout狀態改變時,同時改變ImageView的座標與大小,達到示例效果。使用時很簡單,直接在CoordinatorLayout的XML佈局加上即可。

    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/img"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_gravity="center"
        android:layout_marginTop="-340dp"
        android:src="@drawable/head"
        app:layout_behavior=".FollowBehavior" />

關於CoordinatorLayout的用法到這裏差不多也就結束了,希望能對你有所幫助,如有不足與錯誤的地方隨時向我提問,我會認真採納。

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