Mastering the Coordinator Layout
27 Oct 2015In the Google I/O 15, Google released a new support library which implements several components closely related with the Material Design’s spec, among these components you can find new ViewGroups like the AppbarLayout
, CollapsingToolbarLayout
and CoordinatorLayout
.
Well combined and configured, these Viewgroups have a lot of potential. Therefore, I have decided to write a post with some configurations and tips.
CoordinatorLayout
As its name suggests, the goal and philosophy of this ViewGroup is to coordinate the views that are inside it.
Consider the following picture:
In this example, views are coordinated with each other, in a glance, we can see how some Views depend on others. (we’ll talk about this later).
Let’s see one of the simplest structures using the CoordinatorLayout
:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
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:background="@android:color/background_light"
android:fitsSystemWindows="true"
>
<android.support.design.widget.AppBarLayout
android:id="@+id/main.appbar"
android:layout_width="match_parent"
android:layout_height="300dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:fitsSystemWindows="true"
>
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/main.collapsing"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginStart="48dp"
app:expandedTitleMarginEnd="64dp"
>
<ImageView
android:id="@+id/main.backdrop"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:fitsSystemWindows="true"
android:src="@drawable/material_flat"
app:layout_collapseMode="parallax"
/>
<android.support.v7.widget.Toolbar
android:id="@+id/main.toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_collapseMode="pin"
/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:lineSpacingExtra="8dp"
android:text="@string/lorem"
android:padding="@dimen/activity_horizontal_margin"
/>
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.FloatingActionButton
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_margin="@dimen/activity_horizontal_margin"
android:src="@drawable/ic_comment_24dp"
app:layout_anchor="@id/main.appbar"
app:layout_anchorGravity="bottom|right|end"
/>
</android.support.design.widget.CoordinatorLayout>
The CoordinatorLayout
has only three childs: an AppbarLayout
, a scrolleable view, and an anchored FloatingActionButton
.
<CoordinatorLayout>
<AppbarLayout/>
<scrollableView/>
<FloatingActionButton/>
</CoordinatorLayout>
AppBarLayout
Basically, an AppBarLayout
is a LinearLayout
with steroids, their children are placed vertically, with certain parameters children can manage their own behavior when content is scrolled.
It might sound a bit confusing at first. Let’s try with a gif:
The AppBarLayout
in this case is the blue view, placed under the collapsing image, it contains a Toolbar
. A LinearLayout
with a title and a subtitle, and, finally, a TabLayout
with some tabs.
We can manage the behavior of AppbarLayout
direct childs with the parameter: layout_scrollFlags
. The value: scroll
in this case it’s present in almost all views, if we don’t specify it, childs of the AppbarLayout
will remain static allowing the scrollable content slide behind it.
The value: snap
, allows us to avoid falling into mid-animation-states. That means that animations will always hide or expand its entire height.
The LinearLayout
which contains the title and subtitle will be shown always that user scrolls up, (enterAlways
value), the TabLayout
will be always visible because we don’t have any flag on it.
As you can see ,the real power of an AppbarLayout
is caused by the proper management of the different scroll flags in their views.
<AppBarLayout>
<CollapsingToolbarLayout
app:layout_scrollFlags="scroll|snap"
/>
<Toolbar
app:layout_scrollFlags="scroll|snap"
/>
<LinearLayout
android:id="+id/title_container"
app:layout_scrollFlags="scroll|enterAlways"
/>
<TabLayout /> <!-- no flags -->
</AppBarLayout>
These are all the available parameters acording Google Developers docs. Anyway, my recommendation is to always play by example. There are some Github repositories with different implementations at the end of this article.
AppbarLayout flags
SCROLL_FLAG_ENTER_ALWAYS
: When entering (scrolling on screen) the view will scroll on any downwards scroll event, regardless of whether the scrolling view is also scrolling.
SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED
: An additional flag for ‘enterAlways’ which modifies the returning view to only initially scroll back to it’s collapsed height.
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
: When exiting (scrolling off screen) the view will be scrolled until it is ‘collapsed’.
SCROLL_FLAG_SCROLL
: The view will be scroll in direct relation to scroll events.
SCROLL_FLAG_SNAP
: Upon a scroll ending, if the view is only partially visible then it will be snapped and scrolled to it’s closest edge.
CoordinatorLayout Behaviors
Let’s do a little test, open to Android Studio and create a project with the scrolling template, at the time of the writting is Scrolling Activity, without touching anything, we compile it and this is what we find:
If we review the generated code, there are no layouts nor java classes that have something implemented related with the Fab’s scale animation on scroll. Why is animated then?
The answer can be found inside the FloatingActionButton
source code, since Android Studio a java decompiler for free, with ctrl/cmd + click
we are able to check the source and see what happens:
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Floating action buttons are used for a
* special type of promoted action.
* They are distinguished by a circled icon
* floating above the UI and have special motion behaviors
* related to morphing, launching, and the transferring anchor point.
*
* blah.. blah..
*/
@CoordinatorLayout.DefaultBehavior(
FloatingActionButton.Behavior.class)
public class FloatingActionButton extends ImageButton {
...
public static class Behavior
extends CoordinatorLayout.Behavior<FloatingActionButton> {
private boolean updateFabVisibility(
CoordinatorLayout parent, AppBarLayout appBarLayout,
FloatingActionButton child {
if (a long condition) {
// If the anchor's bottom is below the seam,
// we'll animate our FAB out
child.hide();
} else {
// Else, we'll animate our FAB back in
child.show();
}
}
}
...
}
Who is in charge of that scale animation is a new element introduced with the design library called Behavior
. In this case, a CoordinatorLayout.Behavior<FloatingAcctionButton>
, which depending on some factors including the scroll, shows the FAB or not, interesting, right?.
###SwipeDismissBehavior
Keep diving into the code, if you look inside the widget package of the design support library, we’ll find a public class called: SwipeDismissBehavior
. With this new Behavior
we can very easily implement the functionality of swipe to dismiss in our layouts with a CoordinatorLayout
:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_swipe_behavior);
mCardView = (CardView) findViewById(R.id.swype_card);
final SwipeDismissBehavior<CardView> swipe
= new SwipeDismissBehavior();
swipe.setSwipeDirection(
SwipeDismissBehavior.SWIPE_DIRECTION_ANY);
swipe.setListener(
new SwipeDismissBehavior.OnDismissListener() {
@Override public void onDismiss(View view) {
Toast.makeText(SwipeBehaviorExampleActivity.this,
"Card swiped !!", Toast.LENGTH_SHORT).show();
}
@Override
public void onDragStateChanged(int state) {}
});
LayoutParams coordinatorParams =
(LayoutParams) mCardView.getLayoutParams();
coordinatorParams.setBehavior(swipe);
}
Custom Behaviors
To create a custom Behavior
It isn’t as difficult as it may seem, to begin we must take into account two core elements: child and dependency.
Childs and dependencies
The child is the view that enhances behavior, dependency who will serve as a trigger to interact with the child element. See this example, the child would be the ImageView
and the dependency would be the Toolbar
, in that way, if the Toolbar
moves, the ImageView
will move too.
Now that we have defined the concepts we can speak of implementation, the first step is to extend of: CoordinatorLayout.Behavior<T>
, been T
the class that belongs the view that interests us coordinate, in this case an ImageView, after that we must override these methods:
- layoutDependsOn
- onDependentViewChanged
The method: layoutDependsOn
will be called every time that something happens in the layout, what we must do to return true
once we identify the dependency, in the example, this method is automatically fired when the user scrolls (because the Toolbar
will move), in that way we can make our child sight react accordingly.
@Override
public boolean layoutDependsOn(
CoordinatorLayout parent,
CircleImageView, child,
View dependency) {
return dependency instanceof Toolbar;
}
Whenever layoutDependsOn
returns true
the second onDependentViewChanged
will be called. Here is where we must to implement our animations, translations or movements always related with the provided dependency.
public boolean onDependentViewChanged(
CoordinatorLayout parent,
CircleImageView avatar,
View dependency) {
modifyAvatarDependingDependencyState(avatar, dependency);
}
private void modifyAvatarDependingDependencyState(
CircleImageView avatar, View dependency) {
// avatar.setY(dependency.getY());
// avatar.setBlahBlat(dependency.blah / blah);
}
All together:
public static class AvatarImageBehavior
extends
CoordinatorLayout.Behavior<CircleImageView> {
@Override
public boolean layoutDependsOn(
CoordinatorLayout parent,
CircleImageView, child,
View dependency) {
return dependency instanceof Toolbar;
}
public boolean onDependentViewChanged(
CoordinatorLayout parent,
CircleImageView avatar,
View dependency) {
modifyAvatarDependingDependencyState(avatar, dependency);
}
private void modifyAvatarDependingDependencyState(
CircleImageView avatar, View dependency) {
// avatar.setY(dependency.getY());
// avatar.setBlahBlah(dependency.blah / blah);
}
}
Resources
- Coordinator Behavior Example - Github
- Coordinator Examples - Github
- Introduction to coordinator layout on Android - Grzesiek Gajewski