- 原文地址:Handling Scrolls with CoordinatorLayout
- 原文作者:CODEPATH
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:Feximin
總覽
CoordinatorLayout 擴充套件了完成 Google's Material Design 中的多種滾動效果的能力。目前,此框架提供了幾種不需要寫任何自定義動畫程式碼就可以(使動畫)工作的方式。這些效果包括:
- 上下滑動 Floating Action Button 以給 Snackbar 提供空間。
- 將 Toolbar 或 header 展開或者收起從而為主內容區提供空間。
- 控制哪一個 view 以何種速率進行展開或收起,包括視差滾動效果動畫。
程式碼示例
來自 Google 的 Chris Banes 將 CoordinatorLayout
和 design support library 中其他的特性放在一起做了一個酷炫的 demo。
在 github 上可以檢視完整原始碼。這個專案是最容易理解 CoordinatorLayout
的方式之一。
設定
首先要確保遵循 Design Support Library 的說明。
Floating Action Button 和 Snackbar
CoordinatorLayout 可以通過使用 layout_anchor
和 layout_gravity
屬性來建立懸浮效果。更多資訊請參見 Floating Action Buttons 指南。
當渲染一個 Snackbar 時,它通常出現在可見螢幕的底部。Floating action button 必須上移以便騰出空間。
只要 CoordinatorLayout 被用作主佈局,這個動畫效果就會自動出現。Float action button 有一個預設的 behavior 可以在檢測到 Snackbar 被加入的同時將這個 button 向上移動 Snackbar 的高度。
<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 作為主佈局容器。
<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
標誌。這個標誌必須與enterAlways
、enterAlwaysCollapsed
、 exitUntilCollapsed
或者 snap
一同使用:
enterAlways
:向上滾動時 view 變得可見。此標誌在從一個列表的底部滑動並且希望只要一向上滑動Toolbar
就顯示這種情況下是很有用的。Ps:這裡所說的 scrolling up 應該指的是 list 的滾動條向上滑動而不是上滑的手勢。
通常,只有當 list 滑到頂部的時候
Toolbar
才會顯示,如下所示:enterAlwaysCollapsed
:通常只有當使用了enterAlways
,Toolbar
才會在你向下滑的時候繼續展開:假設你宣告瞭
enterAlways
並且已經設定了一個minHeight
,你也可以使用enterAlwaysCollapsed
。如果這樣設定了,你的 view 只會顯示出這個最低高度。只有當滑到頭的時候那個 view 才會展開到它的完全高度:exitUntilCollapsed
:當設定了scroll
標誌時,下滑通常會引起全部內容的移動:通過指定
minHeight
和exitUntilCollapsed
,剩餘內容開始滾動之前將首先達到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
版本中已經支援底部表了。支援的底部表有兩種型別:persistent 和 modal。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_DRAGGING
,STATE_SETTLING
,和 STATE_HIDDEN
。更多內容,請看 底部表的另一篇教程。
Modal 形式的底部表
Modal 形式的底部表基本上是從底部滑入的 Dialog Fragments。關於如何建立這種型別的 fragment 可以檢視本文。你應該繼承 BottomSheetDialogFragment
而不是 DialogFragment
。
高階的底部表示例
有很多複雜的使用了 floating action button 的底部表的例子,button 隨著使用者滑動或展開或收縮或改變表狀態。最著名的例子就是使用了多階表的 Google Maps:
下述教程和程式碼示例可以幫助你實現這些更加複雜的效果:
CustomBottomSheetBehavior Sample - 描述了在底部表滑動時三種狀態來回切換。參考相關 stackoverflow 博文。
Grafixartist Bottom Sheet Tutorial - 關於在底部表滑動時如何定位 floating action button 以及對其使用動畫的教程。
你可以閱讀本文來進一步討論如何模擬 Google Map 滑動期間狀態改變的效果。
為了得到預期的效果可能需要相當多的實驗。對於某些特定的用例,你可能會發現下面列出的第三方庫是一種更簡單的選擇。
可選的第三方底部表
除了 design support library 中提供的官方底部表,有幾個可選的非常流行的第三方庫,他們在某些特定用法下更容易配置和使用:
以下是最常見的選擇和相關的例子:
- AndroidSlidingUpPanel - 一個廣泛流行的實現了底部表的方法,這應當被視為官方的另一種方案。
- Flipboard/bottomsheet - 另一個在官方方案發布前非常流行的可選方案。
- ThreePhasesBottomSheet - 利用第三方庫來建立一個多階底部表的示例程式碼。
- Foursquare BottomSheet Tutorial - 概述如何用第三方底部表來實現在老版本的 Foursquare 中使用的效果。
在官方的 persistent modal 表和這些第三方的替代方案之間,你應該可以通過足夠的實驗來實現任何想要的效果。
CoordinatorLayout 故障解決
CoordinatorLayout
非常強大但容易出錯。如果你在使用 behavior 時遇到了問題,請檢視下面的建議:
- 關於如何高效使用 CoordinatorLayout 的例子請仔細參考 cheesesquare 原始碼。這個倉庫是一個被 Google 持續更新的示例倉庫,反映了 behavior 的最佳實踐。尤其是 layout for a tabbed ViewPager list 和 this for a layout for a detail view 這兩個。可以仔細比較一下你的程式碼與 cheesesquare 的原始碼。
- 確保在
CoordinatorLayout
的直接子 view 上使用了app:layout_behavior="@string/appbar_scrolling_view_behavior"
屬性。例如,在一個下拉重新整理的例子中,這個屬性應該放在包含了RecyclerView
的SwipeRefreshLayout
中而不是第二層以下的後代中。 - 在一個使用了內部有 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.Behavior 和 FloatingActionButtion.Behavior 這兩個示例。
第三方滾動和視差效果庫
除了使用上述的 CoordinatorLayout
,還可以檢視這些流行的第三方庫來實現 ScrollView
, ListView
, ViewPager
和RecyclerView
間的滾動和視差效果。
將 Google Map 嵌入 AppBarLayout
由於這個已被確認的 issue,目前在 AppBarLayout
中還不支援使用 Google Map。在 v23.1.0 版本的 support design library 的更新中提供了一個 setOnDragListener()
方法,如果在此佈局中需要拖拽效果的話,這個方法將非常有用。然而,它似乎不影響滾動,如這篇博文所述。
參考
- android-developers.blogspot.com/2015/05/and…
- android-developers.blogspot.com/2016/02/and…
- code.tutsplus.com/articles/ho…
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。