(译)使用CoordinatorLayout处理滚动

来源:互联网 发布:ubuntu智能手机 编辑:程序博客网 时间:2024/05/20 13:16

原文链接: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:


下面的教程和示例应该可以帮助实现更复杂的效果:

  • CustomBottomSheetBehavior Sample——演示了滑动bottom sheet时三态的改变。详细说明参考related stackoverflow post。
  • Grafixartist Bottom Sheet Tutorial——关于bottom sheet滑动时怎样移动FAB的位置的教程。
  • 你可以看看stackoverflow post关于怎样实现Google地图滚动时修改状态。

多多实验才能获取预期的效果。对于特定用例,你可以从下面列出的第三方类库中选择。

可选择的第三方BOTTOM SHEET

除了官方在design support library中提供的bottom sheet,这有几个非常受欢迎的可选择的第三方的类库,对于特定用例很方便使用和配置:

下面为最常见的选择和相关例子:

  • AndroidSlidingUpPanel——广受欢迎的实现bottom sheet的方法被认为是最接近官方方法的选择。
  • Flipboard/bottomsheet——除官方bottom sheet外另一个非常受欢迎的类库,在官方解决方案发布之前被广泛使用。
  • ThreePhasesBottomSheet——利用第三方类库创建多相bottom sheet的示例代码。
  • Foursquare BottomSheet Tutorial——概述了怎样使用第三方bottom sheets在Foursquare老版本中实现对应效果。

学习官方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.Behavior和FloatingActionButtion.Behavior的例子。

第三方滚动和视差

除了向上面所说使用CoordinatorLayout,你也可以看看这些受欢迎的第三方类库对ScrollViewListViewViewPagerRecyclerView的滚动视差效果。

在AppBarLayout中引入Google地图

在这个issue已经明确目前无法在AppBarLayout中支持Google Maps fragment。support design library v23.1.0中提供了setOnDragListener()方法,如果在布局中需要拖拽效果这将会很有用。然而,正如这篇文章所说它并不会影响滚动。

参考

  • http://android-developers.blogspot.com/2015/05/android-design-support-library.html
  • http://android-developers.blogspot.com/2016/02/android-support-library-232.html
  • http://code.tutsplus.com/articles/how-to-use-bottom-sheets-with-the-design-support-library--cms-26031
原创粉丝点击