Android UI進階之旅12--Material Design之CoordinatorLayout

CoordinatorLayout簡介

CoordinatorLayout是一個繼承于ViewGroup的布局容器。CoordinatorLayout監聽滑動子控件的滑動通過Behavior反饋到其他子控件并執行一些動畫。簡單來說,就是通過協調并調度里面的子控件或者布局來實現觸摸(一般是指滑動)產生一些相關的動畫效果。
其中,view的Behavior是通信的橋梁,我們可以通過設置view的Behavior來實現觸摸的動畫調度。

注意:滑動控件指的是:RecyclerView/NestedScrollView/ViewPager,意味著ListView、ScrollView不行。

示例

下面介紹一些常見的示例,進一步介紹CoordinatorLayout與其他控件的配合使用,從而做出更好的效果。

示例一、CoordinatorLayout與FloatingActionButton、Snackbar

FloatingActionButton隨列表滾動的動畫

在上一篇文章當中,我們使用CoordinatorLayout實現了FloatingActionButton隨著界面的滑動而進行相應的顯示與隱藏動畫的效果,我們可以自己通過監聽滑動事件實現,同理,我們也可以通過根布局使用CoordinatorLayout來實現,給FloatingActionButton自定義了一個Behavior(實質也是監聽滑動事件)。

Snackbar彈出之后被FloatingActionButton遮擋的問題

在一個界面中,既有FloatingActionButton,又有Snackbar的時候,那么Snackbar的彈出可能就會擋住FloatingActionButton。例如:

fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Snackbar snackbar = Snackbar.make(v, "是否打開GPS?", Snackbar.LENGTH_INDEFINITE);
        snackbar.setAction("好的", new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        }).show();
    }
});

那么最好的解決辦法就是使用CoordinatorLayout來作為根布局,從而解決這個問題。我們可以在Snackbar的showView方法中看到Behavior相關的操作:

final void showView() {
    if (mView.getParent() == null) {
        final ViewGroup.LayoutParams lp = mView.getLayoutParams();

        if (lp instanceof CoordinatorLayout.LayoutParams) {
            // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
            final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;

            final Behavior behavior = new Behavior();
            behavior.setStartAlphaSwipeDistance(0.1f);
            behavior.setEndAlphaSwipeDistance(0.6f);
            behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
            behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
                @Override
                public void onDismiss(View view) {
                    view.setVisibility(View.GONE);
                    dispatchDismiss(Callback.DISMISS_EVENT_SWIPE);
                }

                @Override
                public void onDragStateChanged(int state) {
                    switch (state) {
                        case SwipeDismissBehavior.STATE_DRAGGING:
                        case SwipeDismissBehavior.STATE_SETTLING:
                            // If the view is being dragged or settling, cancel the timeout
                            SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
                            break;
                        case SwipeDismissBehavior.STATE_IDLE:
                            // If the view has been released and is idle, restore the timeout
                            SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
                            break;
                    }
                }
            });
            clp.setBehavior(behavior);
            // Also set the inset edge so that views can dodge the snackbar correctly
            clp.insetEdge = Gravity.BOTTOM;
        }

        mTargetParent.addView(mView);
    }
    //...省略
}
在Snackbar的博客中,我們就已經知道了:Snackbar在選擇錨點的時候,如果遇到了CoordinatorLayout,那么就會默認選擇它作為最合適的父容器。

示例二、CoordinatorLayout與AppBarLayout聯合使用

AppBarLayout是一個繼承LinearLayout的布局容器,它與CoordinatorLayout聯合使用就可以實現一些動態效果(例如標題欄滑出去)。

在使用AppBarLayout的時候,AppBarLayout里面的子控件需要設置一個scrollFlags屬性:

app:layout_scrollFlags="scroll"

flag包括:

    scroll: 里面所有的子控件想要當滑出屏幕的時候view都必須設置這個flag,沒有設置flag的view將被固定在屏幕頂部。
    enterAlways:一旦往下滑,就會馬上出現
    enterAlwaysCollapsed:當你的視圖設置了minHeight屬性的時候,那么視圖只能以最小高度進入,
                只有當滾動視圖到達頂部時才擴大到完整高度。
    exitUntilCollapsed:滾動退出屏幕,最后折疊在頂端。
    snap:
實現標題欄滑出去效果

下面先來布局文件:

<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.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipChildren="false"
        android:clipToPadding="false"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:paddingTop="?attr/actionBarSize">

        <android.support.v7.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            app:divider="@drawable/abc_list_divider_mtrl_alpha"
            app:dividerPadding="10dp"
            app:showDividers="middle">

            <!--這里放置大量控件使得NestedScrollView可以滑動-->

        </android.support.v7.widget.LinearLayoutCompat>

    </android.support.v4.widget.NestedScrollView>

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways"
            app:title="標題欄"
            app:titleTextColor="#fff"/>

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

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

根布局使用了CoordinatorLayout,然后放置了一個用于產生滾動的NestedScrollView。這里需要說明的是,NestedScrollView相對于傳統的ScrollView來說,NestedScrollView解決了一些事件沖突的問題(例如內嵌ListView)。

NestedScrollView需要添加layout_behavior,告知CoordinatorLayout我是滑動的:

app:layout_behavior="@string/appbar_scrolling_view_behavior"

然后NestedScrollView需要添加下面兩句屬性,保證有上padding,同時滑動的時候可以滑到padding區域(不然的話,就會被Toolbar遮擋住了):

android:paddingTop="?attr/actionBarSize"
android:clipChildren="false"
android:clipToPadding="false"

然后我們需要一個標題欄,我們用AppBarLayout對Toolbar進行包裹:

<android.support.design.widget.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="?attr/colorPrimary"
        app:layout_scrollFlags="scroll|enterAlways"
        app:title="標題欄"
        app:titleTextColor="#fff"/>

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

被包裹的Toolbar需要設置scrollFlags屬性,證明Toolbar可以滑出去,并且回滑的時候可以馬上滑回來:

app:layout_scrollFlags="scroll|enterAlways"
AppBarLayout還可以添加其他各種各樣的控件,里面TabLayout等等,不需要滑出去的話就不添加scrollFlags,在下面一個部分會有所體現。
使用ViewPager+TabLayout+Fragment+AppBarLayout實現傳統的APP架構

同樣的,看布局文件:

<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="wrap_content">

        <android.support.v7.widget.Toolbar
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways"
            app:title="標題欄"
            app:titleTextColor="#fff"
            >
        </android.support.v7.widget.Toolbar>

        <android.support.design.widget.TabLayout
            android:id="@+id/tablayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:tabGravity="center"
            app:tabIndicatorColor="#4ce91c"
            app:tabIndicatorHeight="5dp"
            app:tabMode="scrollable"
            app:tabSelectedTextColor="#4ce91c"
            app:tabTextColor="#ccc"
            />
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/vp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

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

根布局還是用CoordinatorLayout。然后寫一個全屏的ViewPager用來存放Fragment,由于CoordinatorLayout是支持ViewPager的,因此直接添加:

app:layout_behavior="@string/appbar_scrolling_view_behavior"

同理我們還需要一個AppBarLayout,里面包裹標題欄Toolbar以及TabLayout,其中TabLayout沒有設置scrollFlags因此不會滑出去。至于Java代碼這里就不再贅述了。

示例三、CoordinatorLayout與CollapsingToolbarLayout結合使用

在示例二的基礎之上,增加一個CollapsingToolbarLayout:

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

    <android.support.design.widget.CollapsingToolbarLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:contentScrim="?attr/colorPrimary"
        app:layout_scrollFlags="scroll|exitUntilCollapsed"
        app:title="標題欄"
        app:titleTextColor="#fff">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:src="@drawable/test"
            app:layout_collapseMode="parallax"
            app:layout_collapseParallaxMultiplier="0.5"/>

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

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

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

屬性分析:

  1. 給ImageView設置的layout_collapseMode是CollapsingToolbarLayout特有的屬性。其中parallax是視差動畫效果。layout_collapseParallaxMultiplier是設置視差動畫的程度;none:沒有任何效果;pin:固定模式,在折疊的時候最后固定在頂端。
  2. contentScrim是折疊之后的背景顏色。
  3. layout_scrollFlags屬性需要給CollapsingToolbarLayout設置exitUntilCollapsed,使得頁面滑動的時候Toolbar可以留在頂端。

自定義Behavior

自定義Behavior的情形:

  1. 某個View需要監聽另外一個View的狀態(位置、大小、顯示狀態)。
  2. 某個View需要監聽CoordinateLayout里面的滑動狀態。

情形一例子、兩個控件同時動作

這時候,child需要監聽dependency的狀態,并且作出相應的動作。

  1. 需要重寫layoutDependsOn判斷監聽誰,這里可以巧妙利用Tag。
  2. 需要重寫onDependentViewChanged,child的動作根據dependency的狀態進行相應。

代碼如下:

public class MyBehavior1 extends CoordinatorLayout.Behavior<View> {

    private static final String TAG = MyBehavior1.class.getSimpleName();

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

    /**
     * 用來決定需要監聽哪些控件或者容器的狀態(1.知道監聽誰;2.什么狀態改變)
     * CoordinatorLayout parent ,父容器
     * View child, 子控件---需要監聽dependency這個view的視圖們---觀察者
     * View dependency,你要監聽的那個View
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child,
                                   View dependency) {
        //還可以根據ID或者TAG來判斷需要監聽哪一個子控件
        return (dependency instanceof TextView && dependency.getTag().toString().equals("dependent"))
                || super.layoutDependsOn(parent, child, dependency);
    }

    /**
     * 當被監聽的view發生改變的時候回調
     * 可以在此方法里面做一些響應的聯動動畫等效果。
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
                                          View dependency) {
        //獲取被監聽的view的狀態---垂直方向位置
        int offset = dependency.getTop() - child.getTop();
        //讓被監聽的child進行平移、旋轉等操作
        ViewCompat.offsetTopAndBottom(child, offset);
        child.animate().rotation(child.getTop() * 20);
        return true;
    }
}

布局文件如下:

<android.support.design.widget.CoordinatorLayout>

    <TextView
        android:id="@+id/tv_dependent"
        android:tag="dependent"
        android:text="被觀察--dependent"/>

    <TextView
        android:text="觀察者" app:layout_behavior="com.nan.advancedui.coordinatorlayout.mybehavior.MyBehavior1"
        />

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

我們點擊tv_dependent,進行平移,那么觀察者就會平移和旋轉:

findViewById(R.id.tv_dependent).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ViewCompat.offsetTopAndBottom(v, 9);
    }
});

情形二例子、兩個滑動控件同步

與FloatingActionButton的自定義Behavior一樣,需要重寫:

  1. onStartNestedScroll判斷需要監聽什么方向的滑動。
  2. onNestedPreScroll、onNestedFling等進行相應動作。

例子如下:

public class MyBehavior2 extends CoordinatorLayout.Behavior<View> {

    private static final String TAG = MyBehavior2.class.getSimpleName();

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

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                                       View child, View directTargetChild, View target,
                                       int nestedScrollAxes) {
        return (nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL)
                || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout,
                                  View child, View target, int dx, int dy, int[] consumed) {
        int scrollY = target.getScrollY();
        child.setScrollY(scrollY);
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout,
                                 View child, View target, float velocityX, float velocityY,
                                 boolean consumed) {
        // 快速滑動的慣性移動(松開手指后還會有滑動效果)
        ((NestedScrollView) child).fling((int) velocityY);
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }
}

看看布局文件:

<android.support.design.widget.CoordinatorLayout>

    <android.support.v4.widget.NestedScrollView>
        ...省略
    </android.support.v4.widget.NestedScrollView>


    <android.support.v4.widget.NestedScrollView
        app:layout_behavior="com.nan.advancedui.coordinatorlayout.mybehavior.MyBehavior2">
        ...省略
    </android.support.v4.widget.NestedScrollView>

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

最終結果就是NestedScrollView同步的滑動。

如果覺得我的文字對你有所幫助的話,歡迎關注我的公眾號:

公眾號:Android開發進階

我的群歡迎大家進來探討各種技術與非技術的話題,有興趣的朋友們加我私人微信huannan88,我拉你進群交(♂)流(♀)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容