用 CoordinatorLayout 處理滾動

總覽

CoordinatorLayout 擴展了完成 Google's Material Design 中的多種滾動效果的能力。目前,此框架提供了幾種不須要寫任何自定義動畫代碼就能夠(使動畫)工做的方式。這些效果包括:html

  • 上下滑動 Floating Action Button 以給 Snackbar 提供空間。

  • 將 Toolbar 或 header 展開或者收起從而爲主內容區提供空間。

  • 控制哪個 view 以何種速率進行展開或收起,包括視差滾動效果動畫。

代碼示例

來自 Google 的 Chris Banes 將 CoordinatorLayoutdesign support library 中其餘的特性放在一塊兒作了一個酷炫的 demo。前端

在 github 上能夠查看完整源碼。這個項目是最容易理解 CoordinatorLayout 的方式之一。java

設置

首先要確保遵循 Design Support Library 的說明。react

Floating Action Button 和 Snackbar

CoordinatorLayout 能夠經過使用 layout_anchorlayout_gravity 屬性來建立懸浮效果。更多信息請參見 Floating Action Buttons 指南。android

當渲染一個 Snackbar 時,它一般出如今可見屏幕的底部。Floating action button 必須上移以便騰出空間。ios

只要 CoordinatorLayout 被用做主佈局,這個動畫效果就會自動出現。Float action button 有一個默認的 behavior 能夠在檢測到 Snackbar 被加入的同時將這個 button 向上移動 Snackbar 的高度。git

<android.support.design.widget.CoordinatorLayout
        android:id="@+id/main_content"
        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.v7.widget.RecyclerView
         android:id="@+id/rvToDoList"
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>

   <android.support.design.widget.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:layout_margin="16dp"
        android:src="@mipmap/ic_launcher"
        app:layout_anchor="@id/rvToDoList"
        app:layout_anchorGravity="bottom|right|end"/>
 </android.support.design.widget.CoordinatorLayout>
複製代碼

展開與收起 Toolbar

首先確保你使用的不是過期的 ActionBar。並確保遵循了 將 ToolBar 用做 ActionBar 指南。還要確保的是以 oordinatorLayout 做爲主佈局容器。github

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

      <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

</android.support.design.widget.CoordinatorLayout>
複製代碼

響應滾動事件

接下來,咱們必須使用一個叫作 AppBarLayout 的容器佈局來使 ToolBar 響應滾動事件:編程

<android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/detail_backdrop_height"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        android:fitsSystemWindows="true">

  <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

 </android.support.design.widget.AppBarLayout>
複製代碼

注意:根據官方的 Google 文檔,目前 AppBarLayout 須要做爲直接子元素被嵌入 CoordinatorLayout 中。後端

而後,咱們須要在 AppBarLayout 和 指望被滾動的 View 之間定義一個關聯。在 RecyclerView 或其餘相似 NestedScrollView 這樣的能夠嵌套滾動的 View 中加入 app:layout_behavior。支持庫中有一個映射到 AppBarLayout.ScrollingViewBehavior 的特殊字符串資源 @string/appbar_scrolling_view_behavior,它能夠在某個特定的 view 上發生滾動事件時通知 AppBarLayout。Behavior 必須創建在觸發(滾動)事件的 view 上。

<android.support.v7.widget.RecyclerView
        android:id="@+id/rvToDoList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
複製代碼

當 CoordinatorLayout 發現 RecyclerView 中聲明瞭這一屬性,它就會搜索包含在其下的其餘 view 看有沒有與這個 behavior 關聯的任何相關 view。在這種特殊狀況下 AppBarLayout.ScrollingViewBehavior 描述了 RecyclerView 和 AppBarLayout 之間的依賴關係。RecyclerView 上的任何滾動事件都將觸發 AppBarLayout 或任何包含在其中的 view 的佈局發生變化。

RecyclerView 的滾動事件觸發了 AppBarLayout 中用 app:layout_scrollFlags 屬性聲明的 view 發生變化:

<android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

            <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.design.widget.AppBarLayout>
複製代碼

若要使任一滾動效果生效,必須啓用 app:layout_scrollFlags 屬性中的 scroll 標誌。這個標誌必須與enterAlwaysenterAlwaysCollapsedexitUntilCollapsed 或者 snap 一同使用:

  • enterAlways:向上滾動時 view 變得可見。此標誌在從一個列表的底部滑動而且但願只要一貫上滑動 Toolbar 就顯示這種狀況下是頗有用的。

    Ps:這裏所說的 scrolling up 應該指的是 list 的滾動條向上滑動而不是上滑的手勢。

    一般,只有當 list 滑到頂部的時候 Toolbar 纔會顯示,以下所示:

  • enterAlwaysCollapsed:一般只有當使用了 enterAlwaysToolbar 纔會在你向下滑的時候繼續展開:

    假設你聲明瞭 enterAlways 而且已經設置了一個 minHeight,你也可使用 enterAlwaysCollapsed。若是這樣設置了,你的 view 只會顯示出這個最低高度。只有當滑到頭的時候那個 view 纔會展開到它的徹底高度:

  • exitUntilCollapsed:當設置了 scroll 標誌時,下滑一般會引發所有內容的移動:

    經過指定 minHeightexitUntilCollapsed,剩餘內容開始滾動以前將首先達到 Toolbar 的最小高度,而後退出屏幕:

  • snap:使用這一選項將由其決定在 view 只有部分減時所執行的功能。若是滑動結束時 view 的高度減小的部分小於原始高度的 50%,那麼它將回到最初的位置。若是這個值大於它的 50%,它將徹底消失。

注意:在你腦海中要將使用了 scroll 標誌位的 view 放在首位。這樣,被摺疊的 view 將會首先退出,留下在頂部固定着的元素。

至此,你應該意識到這個 ToolBar 響應了滾動事件。

建立摺疊效果

若是想建立摺疊 ToolBar 的效果,咱們必須將 ToolBar 包含在 CollapsingToolbarLayout 中:

<android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
    <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:expandedTitleMarginEnd="64dp"
            app:expandedTitleMarginStart="48dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <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.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
複製代碼

如今結果應該顯示爲:

一般,咱們會設置 Toolbar 的標題。如今,咱們須要在 CollapsingToolBarLayout 而不是 Toolbar 上設置標題。

CollapsingToolbarLayout collapsingToolbar =
              (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
 collapsingToolbar.setTitle("Title");
複製代碼

注意,在使用 CollapsingToolbarLayout 的時候,應該如此文檔所述,將狀態欄設置成半透明(API 19)或者透明(API 21)的。特別是,應該在 res/values-xx/styles.xml 中設置如下樣式:

<!-- res/values-v19/styles.xml -->
<style name="AppTheme" parent="Base.AppTheme">
    <item name="android:windowTranslucentStatus">true</item>
</style>

<!-- res/values-v21/styles.xml -->
<style name="AppTheme" parent="Base.AppTheme">
    <item name="android:windowDrawsSystemBarBackgrounds">true</item>
    <item name="android:statusBarColor">@android:color/transparent</item>
</style>
複製代碼

經過像上面那樣啓用系統欄的半透明效果,你的佈局會將內容填充到系統欄後面,所以你還必須在那些不想被系統欄覆蓋的佈局上使用 android:fitsSystemWindow 。另一種爲 API 19 添加內邊距來避免系統欄覆蓋 view 的方案能夠在這裏查看。

建立視差動畫

CollapsingToolbarLayout 可讓咱們作出更高級的動畫,例如使用一個在摺疊的同時能夠漸隱的 ImageView。在用戶滑動時,標題的高度也能夠改變。

要想建立這種效果的話,咱們須要添加一個 ImageView 並在 ImageView 標籤中聲明 app:layout_collapseMode="parallax" 屬性。

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

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_scrollFlags="scroll|enterAlways" />
            <ImageView
                android:src="@drawable/cheese_1"
                app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax"
                android:minHeight="100dp" />

</android.support.design.widget.CollapsingToolbarLayout>
複製代碼

底部表

在 support design library 的 v23.2 版本中已經支持底部表了。支持的底部表有兩種類型:persistentmodal。Persistent 類型的底部表顯示應用內的內容,而 modal 類型的則顯示菜單或者簡單的對話框。

Persistent 形式的底部表

有兩種方法來建立 Persistent 形式的底部表。第一種是用 NestedScrollView,而後就簡單地將內容嵌到裏面。第二種是額外建立一個嵌入 CoordinatorLayout 中的 RecyclerView。若是 layout_behavior 是預約義好的 @string/bottom_sheet_behavior,那麼這個 RecyclerView 默認是隱藏的。還要注意的是 RecyclerView 應該使用 wrap_content 而不是 match_parent,這是一個新修改,爲的是讓底部欄只佔用必要的而不是所有空間:

<CoordinatorLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/design_bottom_sheet"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/bottom_sheet_behavior">
</CoordinatorLayout>
複製代碼

下一步是建立 RecyclerView。咱們能夠建立一個簡單的只包含一張圖片和文字的 Item,和一個能夠填充這些 items 的適配器。

public class Item {

    private int mDrawableRes;

    private String mTitle;

    public Item(@DrawableRes int drawable, String title) {
        mDrawableRes = drawable;
        mTitle = title;
    }

    public int getDrawableResource() {
        return mDrawableRes;
    }

    public String getTitle() {
        return mTitle;
    }

}
複製代碼

接着,建立適配器:

public class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ViewHolder> {

    private List<Item> mItems;

    public ItemAdapter(List<Item> items, ItemListener listener) {
        mItems = items;
        mListener = listener;
    }

    public void setListener(ItemListener listener) {
        mListener = listener;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new ViewHolder(LayoutInflater.from(parent.getContext())
                .inflate(R.layout.adapter, parent, false));
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.setData(mItems.get(position));
    }

    @Override
    public int getItemCount() {
        return mItems.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        public ImageView imageView;
        public TextView textView;
        public Item item;

        public ViewHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(this);
            imageView = (ImageView) itemView.findViewById(R.id.imageView);
            textView = (TextView) itemView.findViewById(R.id.textView);
        }

        public void setData(Item item) {
            this.item = item;
            imageView.setImageResource(item.getDrawableResource());
            textView.setText(item.getTitle());
        }

        @Override
        public void onClick(View v) {
            if (mListener != null) {
                mListener.onItemClick(item);
            }
        }
    }

    public interface ItemListener {
        void onItemClick(Item item);
    }
}
複製代碼

底部表默認是被隱藏的。咱們須要用一個點擊事件來觸發顯示和隱藏。注意:因爲這個已知的 issue,所以不要嘗試在OnCreate() 方法中展開底部表。

RecyclerView recyclerView = (RecyclerView) findViewById(R.id.design_bottom_sheet); 

// Create your items
ArrayList<Item> items = new ArrayList<>();
items.add(new Item(R.drawable.cheese_1, "Cheese 1"));
items.add(new Item(R.drawable.cheese_2, "Cheese 2"));

// Instantiate adapter
ItemAdapter itemAdapter = new ItemAdapter(items, null);
recyclerView.setAdapter(itemAdapter);

// Set the layout manager
recyclerView.setLayoutManager(new LinearLayoutManager(this));

CoordinatorLayout coordinatorLayout = (CoordinatorLayout) findViewById(R.id.main_content);
final BottomSheetBehavior behavior = BottomSheetBehavior.from(recyclerView);

fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
       if(behavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
         behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
       } else {
         behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
       }
    }
});
複製代碼

你能夠設置佈局屬性 app:behavior_hideable=true 來容許用戶也能夠經過滑動而隱藏底部表。還有一些其餘的屬性,包括:STATE_DRAGGINGSTATE_SETTLING,和 STATE_HIDDEN。更多內容,請看 底部表的另外一篇教程

Modal 形式的底部表

Modal 形式的底部表基本上是從底部滑入的 Dialog Fragments。關於如何建立這種類型的 fragment 能夠查看本文。你應該繼承 BottomSheetDialogFragment 而不是 DialogFragment

高級的底部表示例

有不少複雜的使用了 floating action button 的底部表的例子,button 隨着用戶滑動或展開或收縮或改變表狀態。最著名的例子就是使用了多階表的 Google Maps:

下述教程和代碼示例能夠幫助你實現這些更加複雜的效果:

爲了獲得預期的效果可能須要至關多的實驗。對於某些特定的用例,你可能會發現下面列出的第三方庫是一種更簡單的選擇。

可選的第三方底部表

除了 design support library 中提供的官方底部表,有幾個可選的很是流行的第三方庫,他們在某些特定用法下更容易配置和使用:

如下是最多見的選擇和相關的例子:

在官方的 persistent modal 表和這些第三方的替代方案之間,你應該能夠經過足夠的實驗來實現任何想要的效果。

CoordinatorLayout 故障解決

CoordinatorLayout 很是強大但容易出錯。若是你在使用 behavior 時遇到了問題,請查看下面的建議:

  • 關於如何高效使用 CoordinatorLayout 的例子請仔細參考 cheesesquare 源碼。這個倉庫是一個被 Google 持續更新的示例倉庫,反映了 behavior 的最佳實踐。尤爲是 layout for a tabbed ViewPager listthis for a layout for a detail view 這兩個。能夠仔細比較一下你的代碼與 cheesesquare 的源碼。
  • 確保在 CoordinatorLayout 的直接子 view 上使用了 app:layout_behavior="@string/appbar_scrolling_view_behavior" 屬性。例如,在一個下拉刷新的例子中,這個屬性應該放在包含了 RecyclerViewSwipeRefreshLayout 中而不是第二層如下的後代中。
  • 在一個使用了內部有 items 列表的 ViewPager 的 fragment 和一個父 activity 之間使用協調時,你想像這裏描述的那樣在ViewPager 上添加 app:layout_behavior 屬性,認爲這樣就能夠將 pager 中的滾動事件向上傳遞而後就能夠被CoordinatorLayout 管理。可是,記住,你不該該app:layout_behavior 屬性放到 fragment 或者它內部列表上的任何一個位置。
  • 謹記 ScrollView 不能與 CoordinatorLayout 一塊兒使用。你將須要像這個示例中展現的那樣用 NestedScrollView 來代替。將你的內容包含在 NestedScrollView 中,而後在其上添加 app:layout_behavior 就會使你的滾動行爲預期工做。
  • 確保你的 activity 或者 fragment 的根佈局是 CoordinatorLayout。滾動事件不會響應其餘任何佈局。

使用 CoordinatorLayout 時出錯的方式有不少種,當你發現出錯時能夠在這裏添加提示。

自定義 Behavior

CoordinatorLayout with Floating Action Buttons 這篇文章中討論了一個自定義 behavior 例子。

CoordinatorLayout 的工做方式是經過搜索全部在 XML 中靜態地使用 app:layout_behavior 標籤或者以編程的方式在 View 類中使用 @DefaultBehavior 註解裝飾而定義 CoordinatorLayout Behavior 的子 View。當滾動事件發生時,CoorinatorLayout 嘗試去觸發那些被聲明爲依賴項的子 View。

爲了定義你本身的 CoordinatorLayout Behavior,你應該實現 layoutDependsOn() 和 onDependentViewChanged() 這兩個方法。例如 AppBarLayout.Behavior 就定義了這兩個關鍵方法。此 behavior 用來在滾動事件發生時觸發 AppBarLayout 上的改變。

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

 public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
          // check the behavior triggered
          android.support.design.widget.CoordinatorLayout.Behavior behavior = ((android.support.design.widget.CoordinatorLayout.LayoutParams)dependency.getLayoutParams()).getBehavior();
          if(behavior instanceof AppBarLayout.Behavior) {
          // do stuff here
          }
 }       
複製代碼

理解如何實現這些自定義的 behavior 最好方法是研究 AppBarLayout.BehaviorFloatingActionButtion.Behavior 這兩個示例。

第三方滾動和視差效果庫

除了使用上述的 CoordinatorLayout,還能夠查看這些流行的第三方庫來實現 ScrollViewListViewViewPagerRecyclerView 間的滾動和視差效果。

將 Google Map 嵌入 AppBarLayout

因爲這個已被確認的 issue,目前在 AppBarLayout 中還不支持使用 Google Map。在 v23.1.0 版本的 support design library 的更新中提供了一個 setOnDragListener() 方法,若是在此佈局中須要拖拽效果的話,這個方法將很是有用。然而,它彷佛不影響滾動,如這篇博文所述。

參考

掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索