本文介绍了CoordinatorLayout配合滚动标志的效果和创建视差效果及Bottom Sheet的使用。
原文链接: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_anchor
和layout_gravity
属性创建浮动效果。查看Floating Action Buttons使用指南获取更多信息。
当Snackbar被渲染,它通常出现在屏幕底部。为了显示,FAB必须向上移动提供空间。
只要CoordinatorLayout作为布局的根节点,这个动画效果会自动出现。FAB有一个默认的行为会检查Snackbar被添加并且动画向上移动Snackbar的高度。
1 | <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 | <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 | <android.support.v7.widget.RecyclerView |
当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
标志。这个标志必须和enterAlways
,enterAlwaysCollapsed
,exitUntilCollapsed
,或snap
配合使用:
enterAlways
:当向上滚动时View将可见。这个标志对于从列表底部滚动并且想当向上滚动时尽快显示Toolbar
这种情况是非常有用的。
正常情况下,Toolbar
只会在列表滑动到顶部才会出现正如下图所示:enterAlwaysCollapsed
:正常情况下,当只使用了enterAlways
,当向下滑动Toolbar
会继续展开:
假设声明了enterAlways
并且指定了minHeight
,你也可以指定enterAlwaysCollapsed
。当配置了这个,View只会在最小高度出现。当滑动到顶部View会展开完整高度:
exitUntilCollapsed
:当设置了scroll
标志,向下滚动将导致整个内容的移动:
通过指定minHeight
和exitUntilCollapsed
,到达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
3CollapsingToolbarLayout collapsingToolbar =
(CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
collapsingToolbar.setTitle("Title");
注意当使用CollapsingToolbarLayout
时,状态栏应该设置为translucent (API 19) 或 transparent (API 21) 正如这个文件所示。特别是,应该在res/values-xx/styles.xml
设置为下面的样式:
1 | <!-- res/values-v19/styles.xml --> |
通过如上所示开启透明系统条,布局将会填充系统条后面的区域,因此你也必须为不应该被系统条覆盖的部分布局开启android:fitsSystemWindow
。对于API 19还可以通过添加padding去避免状态条裁剪View,可以从这找到。
创建视差动画
CollapsingToolbarLayout也可以为我们做更高级的动画,例如当它折叠时使一个图片逐渐消失。Title也可以随着用户的滑动改变高度。
为了创建这个效果,我们添加一个ImageView并声明一个app:layout_collapseMode="parallax"
属性。
1 | <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 | <CoordinatorLayout> |
然后创建RecyclerView的元素。我们创建一个简单的Item
包含一张图片和一个文本。
1 | public class Item { |
然后创建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
60public 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 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.design_bottom_sheet); |
你可以设置一个app:behavior_hideable=true
布局属性允许用户滑动bottom sheet隐藏。这还有其它的状态包括:STATE_DRAGGING
,STATE_SETTLING
和STATE_HIDDEN
。想要扩展阅读,你也可以看看另一篇bottom sheet教程。
Modal Sheets
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
11public 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
,你也可以看看这些受欢迎的第三方类库对ScrollView
,ListView
,ViewPager
和RecyclerView
的滚动视差效果。
在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