An sticky button with CoordinatorLayout Behaviors

Juan Mengual
androidxx
4 min readMar 31, 2018

--

Photo by Jodie Morgan on Unsplash

Our UX team came up with a really great design for a product detail page starring a “buy button” which is placed in some position of the vertical scrollable content. Better check the image below.

Uhh… stiiiiicky

There are two key features here:

  • The button scrolls vertically, but cannot scroll down from the bottom of the screen (this is the sticky part)
  • Button has some margins which decrease when scrolling close to the bottom margin of the screen, being zero when the button is sticked at the bottom.

We implemented the first version more than a year ago, using the “anchor” attribute of CoordinatorLayout to automagically move the button vertically and, to my surprise, it takes care of also sticking the button to the bottom.

For the margins, we simply added an scroll listener and did some calculations to adjust the margins when the user was scrolling. As a results, some global variables and a method named “updateButton()” which has to be called not only from the ScrollListener but any other place which changes something of the button (i.e: visibility).

Recently I had to add the same button functionality to a different screen and first I was tempted to just copy and paste the previous solution. It didn’t working since it had some tricks for the specific screen it was planned to, so it was the perfect excuse to try a better approach which can be easily ported to any screen.

First approach with Behaviors (wrong one)

I’d like to take an overviewlook to my first attempt with behaviors, whcih didin’t work. The approach:

  • Keep using CoordinatorLayout anchor property to make the button move and stick at the bottom.
  • Use a Behavior to handle the margins when the scroll is close to the bottom. This is, adding some margin to the LayoutParams depending on the position of the screen.

It worked fine when scrolling but not when flinging. For some reason, when the user flings and you change the LayoutParams of the button, the buttons is not visible until the fling has stopped, then is just appears in the screen. Looks like its skipping the rendering part of the view until the fling is over. More details here: https://stackoverflow.com/questions/49363430/view-not-visible-during-fling-when-using-behavior-to-increase-the-view-width-whe

Final approach

Since changing the LayoutParams and using CoordinatorLayout anchor property doesn’t like each other, our Behavior is gonna be responsible from moving the Button vertically until it sticks to the bottom. As a workmate use to say “show me the code”.

The layouts

The content is divided in two layouts, the scrolling part and the content.

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">

<android.support.design.widget.AppBarLayout>
<android.support.design.widget.CollapsingToolbarLayout>
<android.support.v7.widget.Toolbar/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_scrolling"/>

<Button
android:id="@+id/my_sticky_button"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="my sticky button"
android:background="#ff0000"
android:fitsSystemWindows="true"
android:layout_gravity="center_horizontal|bottom"
/>

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

It’s a classic CoordinatorLayout with nothing else apart from the Button which will be the sticky component. Now the content, which is a NestedScrollView with a View which represent the position of the button in that content. We will use that View to know the position where the button should be (if it wasn’t sticky). That view is called anchor.

The content is just some big views at the beginning and end to have some scrolling space.

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView>

<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/one"
android:layout_width="match_parent"
android:layout_height="1000dp"
android:background="#999"
android:gravity="center"
android:text="top"
app:layout_constraintTop_toBottomOf="parent"/>
<View
android:id="@+id/anchor"
android:layout_marginTop="40dp"
android:layout_height="48dp"
android:layout_width="match_parent"
app:layout_constraintTop_toBottomOf="@id/one"/>

<TextView
android:id="@+id/six"
android:layout_width="match_parent"
android:layout_height="900dp"
android:background="#eee"
android:gravity="center"
android:text="bottom"
app:layout_constraintTop_toBottomOf="@id/anchor"/>
</android.support.constraint.ConstraintLayout>

</android.support.v4.widget.NestedScrollView>

Our View with id anchor will be the one used to know the position the button would have in the scroll. Think about it as a placeholder.

The StickyBehavior

The great thing about behaviors is that you get the chance to react to the scroll happening at any child of the coordinator layout. We will be using ‘onNestedPreScroll’ because we want to know every scroll happening in the Y axis and we don’t care about who is consuming it.

This is whats happening at ‘onNestedPreScroll’

  • Get the position in the screen of the anchor view, which is the position the button would have if not being sticky.
  • Set the vertical position of the sticky view, using the parent.getBottom() value to be sure the View is never below the parent.
  • Set the margins to the sticky view depending also on the vertical position.

The final setup in the Activity

View anchor = findViewById(R.id.anchor);
Button button = findViewById(R.id.my_button);
((CoordinatorLayout.LayoutParams) button.getLayoutParams()).setBehavior(new StickyBottomBehavior(R.id.anchor, getResources().getDimensionPixelOffset(R.dimen.margins)));

And that’s it, the button never scrolls behind the parent bottom and the margins change when approaching the bottom.

To be improved

Would be even better to add the behavior in the layout, so you don’t even need to create the object in Code.

When fling, you could see that the sticky View is moving a few pixels behind that it should be. This was good enough for me at this point, but I would say that you could use an Scroller to calculate the position it should be more accurately.

The value returned by getLocationInWindow() is only accurate when fitSystemWindows =”true”. I need to further research a way of getting the correct value when it is set to false.

That’s it

Thanks for passing by, I hope that this post will make you think about behaviors whenever you have to react to an scrolling component.

The full code is here: https://github.com/juanmeanwhile/StickyButton

--

--