Handling Scrolls with CoordinatorLayout - meswapnilwagh/android_guides GitHub Wiki
[CoordinatorLayout] (https://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.html) extends the ability to accomplish many of the Google's Material Design scrolling effects. Currently, there are several ways provided in this framework that allow it to work without needing to write your own custom animation code. These effects include:
-
Sliding the Floating Action Button up and down to make space for the Snackbar.
-
Expanding or contracting the Toolbar or header space to make room for the main content.
-
Controlling which views should expand or collapse and at what rate, including parallax scrolling effects animations.
Chris Banes from Google has put together a beautiful demo of the CoordinatorLayout
and other design support library features.
The full source code can be found on github. This project is one of the easiest ways to understand CoordinatorLayout
.
Make sure to follow the Design Support Library instructions first.
The CoordinatorLayout can be used to create floating effects using the layout_anchor
and layout_gravity
attributes. See the Floating Action Buttons guide for more information.
When a Snackbar is rendered, it normally appears at the bottom of the visible screen. To make room, the floating action button must be moved up to provide space.
So long as the CoordinatorLayout is used as the primary layout, this animation effect will occur for you automatically. The floating action button has a default behavior that detects Snackbar views being added and animates the button above the height of the 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.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>
The first step is to make sure you are not using the deprecated ActionBar. Make sure to follow the Using the ToolBar as ActionBar guide. Also make sure that the CoordinatorLayout is the main layout container.
<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>
Next, we must make the Toolbar responsive to scroll events using a container layout called AppBarLayout:
<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>
Note: AppBarLayout currently expects to be the first child nested within a CoordinatorLayout according to the official [Google docs] (http://developer.android.com/reference/android/support/design/widget/AppBarLayout.html).
Next, we need to define an association between the AppBarLayout and the View that will be scrolled. Add an app:layout_behavior
to a RecyclerView or any other View capable of nested scrolling such as NestedScrollView. The support library contains a special string resource @string/appbar_scrolling_view_behavior
that maps to AppBarLayout.ScrollingViewBehavior, which is used to notify the AppBarLayout
when scroll events occur on this particular view. The behavior must be established on the view that triggers the event.
<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">
When a CoordinatorLayout notices this attribute declared in the RecyclerView, it will search across the other views contained within it for any related views associated by the behavior. In this particular case, the AppBarLayout.ScrollingViewBehavior
describes a dependency between the RecyclerView and AppBarLayout. Any scroll events to the RecyclerView should trigger changes to the AppBarLayout layout or any views contained within it.
Scroll events in the RecyclerView trigger changes inside views declared within AppBarLayout
by using the app:layout_scrollFlags
attribute:
<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>
The scroll
flag used within the attribute app:layout_scrollFlags
must be enabled for any scroll effects to take into effect. This flag must be enabled along with enterAlways
, enterAlwaysCollapsed
, exitUntilCollapsed
, or snap
:
-
enterAlways
: The view will become visible when scrolling up. This flag is useful in cases when scrolling from the bottom of a list and wanting to expose theToolbar
as soon as scrolling up takes place.Normally, the
Toolbar
only appears when the list is scrolled to the top as shown below: -
enterAlwaysCollapsed
: Normally, when onlyenterAlways
is used, theToolbar
will continue to expand as you scroll down:Assuming
enterAlways
is declared and you have specified aminHeight
, you can also specifyenterAlwaysCollapsed
. When this setting is used, your view will only appear at this minimum height. Only when scrolling reaches to the top will the view expand to its full height: -
exitUntilCollapsed
: When thescroll
flag is set, scrolling down will normally cause the entire content to move:By specifying a
minHeight
andexitUntilCollapsed
, the minimum height of theToolbar
will be reached before the rest of the content begins to scroll and exit from the screen: -
snap
: Using this option will determine what to do when a view only has been partially reduced. If scrolling ends and the view size has been reduced to less than 50% of its original, then this view to return to its original size. If the size is greater than 50% of its sized, it will disappear completely.
Note: Keep in mind to order all your views with the scroll
flag first. This way, the views that collapse will exit first while leaving the pinned elements at the top.
At this point, you should notice that the Toolbar responds to scroll events.
If we want to create the collapsing toolbar effect, we must wrap the Toolbar inside CollapsingToolbarLayout:
<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>
Your result should now appears as:
Normally, we set the title of the Toolbar. Now, we need to set the title on the CollapsingToolBarLayout instead of the Toolbar.
CollapsingToolbarLayout collapsingToolbar =
(CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
collapsingToolbar.setTitle("Title");
Note that when using CollapsingToolbarLayout
, the status bar should be made translucent (API 19) or transparent (API 21) as shown in this file. In particular, the following styles should be set in res/values-xx/styles.xml
as illustrated:
<!-- 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>
By enabling translucent system bars as shown above, your layout will fill the area behind the system bars, so you must also enable android:fitsSystemWindow
for the portions of your layout that should not be covered by the system bars. An additional workaround for API 19 which adds padding to avoid the status bar clipping views can be found here.
The CollapsingToolbarLayout also enables us to do more advanced animations, such as using an ImageView that fades out as it collapses. The title can also change in height as the user scrolls.
To create this effect, we add an ImageView and declare an app:layout_collapseMode="parallax"
attribute to the tag.
<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 are now supported in v23.2
of the support design library. There are two types of bottom sheets supported: persistent and modal. Persistent bottom sheets show in-app content, while modal sheets expose menus or simple dialogs.
The easiest way to create them is using an additional RecyclerView nested inside a CoordinatorLayout
. This RecyclerView
will be hidden by default if the layout_behavior
defined is set using the pre-defined @string/bottom_sheet_behavior
value. Note also that this RecyclerView
should be using wrap_content
instead of match_parent
, which is a new change that allows the bottom sheet to only occupy the necessary space instead of the entire page:
<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>
The next step is to create RecyclerView
elements. We can create a simple Item
that contains an image and a text and an adapter that can inflate these 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;
}
}
Next, we create our adapter:
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);
}
}
The bottom sheet should be hidden by default. We need to use a click event to trigger the show and hide. Note: do not try to expand the bottom sheet inside an OnCreate()
method because of this known issue.
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);
}
}
});
You can set a layout attribute app:behavior_hideable=true
to allow the user to swipe the bottom sheet away too. There are other states including STATE_DRAGGING
, STATE_SETTLING
, and STATE_HIDDEN
. For additional reading, you can see another tutorial on bottom sheets here.
Modal sheets are basically Dialog Fragments that slide from the bottom. See this guide about how to create these types of fragments. Instead of extending from DialogFragment
, you would extend from BottomSheetDialogFragment
.
CoordinatorLayout
is very powerful but error-prone at first. If you are running into issues with coordinating behavior, check the following tips below:
- The best example of how to use coordinator layout effectively is to refer carefully to the source code for cheesesquare. This repository is a sample repo kept updated by Google to reflect best practices with coordinating behaviors. In particular, see the layout for a tabbed ViewPager list and this for a layout for a detail view. Compare your code carefully to the cheesesquare source code.
- When coordinating between a fragment with a list of items inside of a
ViewPager
and a parent activity, you want to put theapp:layout_behavior
property on theViewPager
as outlined here so the scrolls within the pager are bubbled up and can be managed by theCoordinatorLayout
. Note that you should not put thatapp:layout_behavior
property anywhere within the fragment or the list within. - Keep in mind that
ScrollView
does not work withCoordinatorLayout
. You will need to use theNestedScrollView
instead as shown in this example. Wrapping your content in theNestedScrollView
and applying theapp:layout_behavior
property will cause the scrolling behavior to work as expected. - Make sure that the root layout is a CoordinatorLayout. Scrolls will not react to any other layout.
There's a lot of ways coordinating layouts can go wrong. Add tips here as you discover them.
One example of a custom behavior is discussed in using CoordinatorLayout with Floating Action Buttons.
CoordinatorLayout works by searching through any child view that has a CoordinatorLayout Behavior defined either statically as XML with a app:layout_behavior
tag or programmatically with the View class annotated with the @DefaultBehavior
decorator. When a scroll event happens, CoordinatorLayout attempts to trigger other child views that are declared as dependencies.
To define your own a CoordinatorLayout Behavior, the layoutDependsOn() and onDependentViewChanged() should be implemented. For instance, AppBarLayout.Behavior has these two key methods defined. This behavior is used to trigger a change on the AppBarLayout when a scroll event happens.
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
}
}
The best way to understand how to implement these custom behaviors is by studying the AppBarLayout.Behavior and FloatingActionButtion.Behavior examples. Although the source code is not publicly available yet, you can use the decompiler integrated with Android Studio 1.2 to examine how they work by navigating up the source tree.
In addition to using the CoordinatorLayout
as outlined above, be sure to check out these popular third-party libraries for scrolling and parallax effects across ScrollView
, ListView
, ViewPager
and RecyclerView
.
There is currently no way of supporting Google Maps fragment within an AppBarLayout
as confirmed in this issue. Changes in the support design library v23.1.0 now provide a setOnDragListener()
method, which is useful if drag and drop effects are needed within this layout. However, it does not appear to impact scrolling as stated in this blog article.