Playing With Coordinator Layout

Lately I needed to build some kind of overlay screen that looks like the one that shows when you open the phone when you don’t have a lock screen. This is a simple overlay screen that should be dismissable when the user swipe it out from the lower part of the screen, something like this:

When coming to implement it, I thought about building a LinearLayout with 2 views- the content view and the swipe view, than put some touch listener on the swipe view and implement swiping mechanism, than syncing both view to act the same. But when I thought about it more and more I decided that it’s too much work. Searching this kind of code or library out there always bring me to RecycleView swipe to dismiss items, and in one of those search results I saw how to implement this swipe to dismiss item in RecycleView with CoordinatorLayout, so I decided to give it a chance.

CoordinatorLayout is a really great addition in the design support library. Most of you probably already used it with a floating action buttons or snackbars but if you like me- you just put it there, assuming views will move properly, without really diving into how the layout is syncing between them.

So it turns out that CordinatorLayout is pretty powerful, it let you easily define interactions on child views and even between them, those interactions are called behaviors so that each child can behave differently according to things that are happening in the layout. You can do a really cool things with those behaviors, here is one example for a great blog post that I stumble upon my searches about coordinate layout usages.

In our case it suppose to be pretty easy, there is already a swipe to dismiss behavior ready to be used on the lower view, so we just need to attach it to the relevant view. First lets build our layout:

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
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:id="@+id/container"
         android:background="#06ff87"/>

    <TextView
        android:id="@+id/dismiss"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="> Swipe to dismiss > > >"
        android:padding="30dp"
        android:gravity="center"
        android:layout_gravity="center_horizontal|bottom"
        android:background="#3bb0ff"/>

</android.support.design.widget.CoordinatorLayout>

As said, it’s a simple layout with 2 view- one for the content (the green one) and one for the swiping (the blue one).

Now, lets attach the relevant behavior to the relevant view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
View dismiss = findViewById(R.id.dismiss);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) dismiss.getLayoutParams();
SwipeDismissBehavior<View> behavior = new SwipeDismissBehavior<>();
behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
    @Override
    public void onDismiss(final View view) {
        finish();
    }

    @Override
    public void onDragStateChanged(int i) {}
});
params.setBehavior(behavior);

That’s pretty self explained- we declare a new swipe to dismiss behavior, set it’s direction only to right, and finish the activity on dismiss. But there’s on problem- somehow, although we declare this behavior only on the dismiss view, it working on all the view, so also the content view is swipeable as you can see here:

I’m not sure if this is by design or just a bug (after digging in the code I’m afraid this behavior is not intended) but we can easily fix this by creating our own behavior. Of course that we not going to write this whole code from beginning, we’ll use the SwipeDismissBehavior class and just fix the relevant code. I really recommend doing a little investigation and reading the Android code, that’s way you’re fully understand how things really works and get to read an high quality code written by the Android developers, in that case the fix is pretty easy to spot- the SwipeDismissBehavior handle touch events also if those touches not in the view (although there is a check in the onInterceptTouchEvent method and those events should be ignore) so I created a new behavior class just for this fix:

1
2
3
4
5
6
7
8
9
10
/**
 * Handle only touches on this current view
 */
@Override
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
    if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) {
        return super.onTouchEvent(parent, child, event);
    }
    return false;
}

Changing the activity to use SwipeDismissOnlyOnViewBehavior instead of SwipeDismissBehavior is doing the work:

Now we need to sync between the content view to the dismiss view when swiping, so we creating a new behavior just for this. Because this is really simple behavior all we need to implement are 2 functions, in which view this content view depended (layoutDependsOn) and what to do when the dependent view changed (onDependentViewChanged):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
=
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) container.getLayoutParams();
final CoordinatorLayout.Behavior<View> behavior = new CoordinatorLayout.Behavior<View>() {
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency.getId() == R.id.dismiss;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        child.setX(dependency.getX());
        child.setAlpha(dependency.getAlpha());
        return true;
    }
};
params.setBehavior(behavior);

And voila:

Comments