(譯)使用CoordinatorLayout處理滾動

yangxi_001發表於2017-09-19

原文連結:Handling Scrolls with CoordinatorLayout

概述

CoordinatorLayout可以完成很多Google的 Material Design滾動效果。目前,框架中提供了幾種方法讓它工作並且你不需要自己寫動畫程式碼。

這些效果包括:

  • 為Snackbar提供空間向上和向下滑動Floating Action Button。

  • 擴大或收縮Toolbar或header的空間為主要內容提供空間。

  • 控制View應該以什麼樣的速率擴充套件或收縮,包括視差滾動效果動畫。

示例程式碼

來自Google的Chris Banes已經做出了CoordinatorLayout的漂亮的demo和design support library的其它特性。

完整原始碼可以從github上找到。這個工程可以很容易理解CoordinatorLayout

配置

確保根據Design Support Library說明進行配置。

Floating Action Buttons 和 Snackbars

CoordinatorLayout可以通過layout_anchorlayout_gravity屬性建立浮動效果。檢視Floating Action Buttons使用指南獲取更多資訊。

當Snackbar被渲染,它通常出現在螢幕底部。為了顯示,FAB必須向上移動提供空間。

只要CoordinatorLayout作為佈局的根節點,這個動畫效果會自動出現。FAB有一個預設的行為會檢查Snackbar被新增並且動畫向上移動Snackbar的高度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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.v7.widget.RecyclerView>

<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>

擴充套件或收縮Toolbars

首先需要確保沒有使用被棄用的ActionBar。確保根據使用Toolbar作為ActionBar進行配置。也確保CoordinatorLayout作為主要佈局容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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響應滾動事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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中的直接子View。

然後,我們需要定義AppBarLayout和可以滾動的View之間的關係。給RecyclerView或其它可以巢狀滾動的View例如NestedScrollView新增一個app:layout_behavior屬性。support library包含一個特殊的字串資源@string/appbar_scrolling_view_behavior對應AppBarLayout.ScrollingViewBehavior,用於在指定View上發生滾動事件時通知AppBarLayout。這個行為(behavior)必須放在觸發事件的View上。

1
2
3
4
5
<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上,它會根據behavior搜尋被包含的任何有關係的其它View。在這種情況下,AppBarLayout.ScrollingViewBehavior描述了RecyclerView和AppBarLayout之間的依賴。RecyclerView的任何滾動事件都會觸發AppBarLayout或包含在它之內的佈局改變。

RecyclerView的滾動事件會觸發在AppBarLayout內宣告瞭app:layout_scrollFlags屬性的View改變:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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這種情況是非常有用的。

    正常情況下,Toolbar只會在列表滑動到頂部才會出現正如下圖所示:

  • enterAlwaysCollapsed:正常情況下,當只使用了enterAlways,當向下滑動Toolbar會繼續展開:

假設宣告瞭enterAlways並且指定了minHeight,你也可以指定enterAlwaysCollapsed。當配置了這個,View只會在最小高度出現。當滑動到頂部View會展開完整高度:

  • exitUntilCollapsed:當設定了scroll標誌,向下滾動將導致整個內容的移動:

通過指定minHeightexitUntilCollapsed,到達Toolbar的最小高度之前剩下的內容開始滾動並退出螢幕:

  • snap:使用這個選項將決定當一個View只減少一部分時做什麼。當滾動結束並且減少的View的大小比它原先大小的50%小,這個View會返回到它原先的大小,如果比它大小的50%大,它會完全消失。

注意:記住首先給所有的View設定scroll標誌。這樣的話,View退出前產生視差在頂部留下固定元素。

這時候,你應該注意到了Toolbar響應了滾動事件。

建立收縮效果

如果我們想建立toolbar收縮效果,我們必須把Toolbar放到CollapsingToolbarLayout中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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設定title。現在我們需要給CollapsingToolBarLayout設定title而不是Toolbar。

1
2
3
CollapsingToolbarLayout collapsingToolbar =
(CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
collapsingToolbar.setTitle("Title");

注意當使用CollapsingToolbarLayout時,狀態列應該設定為translucent (API 19) 或 transparent (API 21) 正如這個檔案所示。特別是,應該在res/values-xx/styles.xml設定為下面的樣式:

1
2
3
4
5
6
7
8
9
10
<!-- 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還可以通過新增padding去避免狀態條裁剪View,可以從這找到

建立視差動畫

CollapsingToolbarLayout也可以為我們做更高階的動畫,例如當它摺疊時使一個圖片逐漸消失。Title也可以隨著使用者的滑動改變高度。

為了建立這個效果,我們新增一個ImageView並宣告一個app:layout_collapseMode="parallax"屬性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<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>
<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>

Bottom Sheets

Bottom Sheets 在 support design library v23.2 中提供支援。支援兩種型別的bottom sheets:固定(persistent) 和 模態(modal)。固定的bottom sheets顯示應用中的內容,modal sheets顯示一個選單或簡單的對話方塊。


圖 Persistent Modal Sheets(譯者加)


圖 Modal Sheets(譯者加)

PERSISTENT MODAL SHEETS

這有兩種方法建立persistent modal sheets。第一種方法是使用一個NestedScrollView,然後把內容放到這個View中。第二種方法是使用一個RecyclerView嵌入到CoordinatorLayout中。如果layout_behavior使用的是預定義的@string/bottom_sheet_behavior值,那RecyclerView預設會隱藏。注意RecyclerView應該使用wrap_content而不是match_parent,這可以讓bottom sheet只出現在必要的空間而不是整個頁面:

1
2
3
4
5
6
7
8
<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包含一張圖片和一個文字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}

}

然後建立adapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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);
}
}

bottom sheet預設情況下應該是隱藏的。我們需要點選事件觸發顯示和隱藏。注意: 不要嘗試在OnCreate()方法展開bottom sheet因為這個已知問題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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佈局屬性允許使用者滑動bottom sheet隱藏。這還有其它的狀態包括:STATE_DRAGGINGSTATE_SETTLINGSTATE_HIDDEN。想要擴充套件閱讀,你也可以看看另一篇bottom sheet教程

Modal sheets基於Dialog Fragments可以從底部滑動。檢視這篇指南學習怎樣建立這些型別的fragments。不是繼承自DialogFragment,應該繼承BottomSheetDialogFragment

高階BOTTOM SHEET示例

這有很多帶有一個FAB複雜的bottom sheets的例子,可以跟隨使用者的滑動展開或收縮或狀態過渡。最知名的例子就是Google地圖的多相sheet:


下面的教程和示例應該可以幫助實現更復雜的效果:

多多實驗才能獲取預期的效果。對於特定用例,你可以從下面列出的第三方類庫中選擇。

可選擇的第三方BOTTOM SHEET

除了官方在design support library中提供的bottom sheet,這有幾個非常受歡迎的可選擇的第三方的類庫,對於特定用例很方便使用和配置:

下面為最常見的選擇和相關例子:

學習官方persistent modal sheets和第三方類庫的實現,通過足夠的實驗你應該能實現任何你想要的效果。

Coordinated Layouts常見問題

CoordinatorLayout很強大但剛開始很容易出錯。如果你在使用過程中出現了問題,請檢視下面的提示:

  • 怎樣有效地使用coordinator layout最好的例子是參考cheesesquare原始碼。這個倉庫是Google保持更新的示例倉庫代表coordinating behaviors的最佳實踐。尤其是檢視ViewPager list佈局詳情頁佈局。拿你的原始碼和cheesesquare原始碼進行比較。
  • 確保
    app:layout_behavior="@string/appbar_scrolling_view_behavior"屬性應用到了CoordinatorLayout的 直接子View。例如,這有一個下拉重新整理佈局SwipeRefreshLayout中包含一個RecyclerView,這個屬性應該應用到SwipeRefreshLayout而不是第二級子ViewRecyclerView
  • 當coordinating發生在一個ViewPager包含fragment作為item的list和parent activity之間,你應該把app:layout_behavior屬性放在ViewPager上(正如這個檔案所示),因此pager內的滾動是向上突出的並且可以通過CoordinatorLayout進行管理。注意你 不應該 把app:layout_behavior屬性放到fragment或list中的任何地方。
  • 注意ScrollView無法和CoordinatorLayout配合使用。你需要使用NestedScrollView代替就像這個例子所示。把你的內容放到NestedScrollView中並且應用app:layout_behavior屬性可以得到預期的效果。
  • 確保你的activity或fragment的根佈局是CoordinatorLayout。滾動不會響應到其它任何佈局。

導致coordinating layouts出錯的原因有很多種。當你遇到了請新增提示到這裡。

自定義Behaviors

我們已經在CoordinatorLayout with Floating Action Buttons中討論過一個自定義Behaviors的例子。

CoordinatorLayout是通過搜尋在XML中定義了app:layout_behavior屬性或者在View類中新增@DefaultBehavior註解包含CoordinatorLayout Behavior工作的。當發生滾動事件,CoordinatorLayout會嘗試觸發作為依賴宣告的其它子View。

對於自定義CoordinatorLayout Behavior,應該實現layoutDependsOn() 和 onDependentViewChanged()。例如,AppBarLayout.Behavior定義了兩個關鍵方法。這個behavior用於當滾動事件發生時觸發AppBarLayout的改變。

1
2
3
4
5
6
7
8
9
10
11
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
}
}

理解怎樣實現這些自定義行為最好的辦法就是學習AppBarLayout.BehaviorFloatingActionButtion.Behavior的例子。

第三方滾動和視差

除了向上面所說使用CoordinatorLayout,你也可以看看這些受歡迎的第三方類庫ScrollViewListViewViewPagerRecyclerView的滾動視差效果。

在AppBarLayout中引入Google地圖

在這個issue已經明確目前無法在AppBarLayout中支援Google Maps fragment。support design library v23.1.0中提供了setOnDragListener()方法,如果在佈局中需要拖拽效果這將會很有用。然而,正如這篇文章所說它並不會影響滾動。

參考

相關文章