Android Tutorial: Creating Buttons That Appear Conditionally on Scroll
Imagine you’re scrolling down a very long screen (this is a common situation with Privacy Policies). As you scroll down the ‘Accept’ button scrolls off the screen. This is a short but helpful Android Tutorial for fixing this UX problem.
I will show you how to place the button in an alternative position: the bottom of the screen. When the original button reappears, the button at the bottom should disappear (which means that you should never have both buttons visible on the screen).
The support library provides us with two components that are very useful for this Android tutorial:
The button at the bottom of the screen can be implemented with the help of a Bottom Sheet component.
For identifying the scrolling behavior and triggering the appearance of the Bottom Sheet, we can use a Coordinator Layout.
Android Authority has published two quite helpful tutorials, with source code on Github for these two components:
- Bottom Sheets
So, the first step is quite straightforward: add the button at the bottom as a Bottom Sheet.
1<?xml version="1.0" encoding="utf-8"?>2<android.support.design.widget.CoordinatorLayout3 xmlns:android="http://schemas.android.com/apk/res/android"4 xmlns:app="http://schemas.android.com/apk/res-auto"5 android:layout_width="match_parent"6 android:layout_height="match_parent">789 <ScrollView10 android:id="@+id/scroll_view"11 android:layout_width="match_parent"12 android:layout_height="match_parent"13 app:layout_behavior="@string/appbar_scrolling_view_behavior">1415 <LinearLayout16 android:id="@+id/c"17 android:layout_width="match_parent"18 android:layout_height="wrap_content"19 android:gravity="center_horizontal"20 android:orientation="vertical"21 android:paddingBottom="70dp"22 android:paddingLeft="32dp"23 android:paddingRight="32dp"24 android:paddingTop="20dp">2526 <TextView27 android:layout_marginTop="16dp"28 android:text="@string/privacy_header_1"29 style="@style/header"/>3031 <TextView32 style="@style/privacy_text"33 android:layout_marginTop="29dp"34 android:text="@string/privacy_text"/>3536 <Button37 android:id="@+id/got_it"38 android:layout_width="160dp"39 android:layout_height="50dp"40 android:layout_marginTop="29dp"41 android:text="@string/understood"42 android:background="@drawable/rounded_button"43 android:textAppearance="@style/rounded_button_text"/>444546 <TextView47 style="@style/header"48 android:layout_marginTop="48dp"49 android:text="@string/privacy_header_2"/>505152 <TextView53 android:id="@+id/long_long_text"54 style="@style/privacy_text"55 android:layout_marginTop="23dp"56 android:text="@string/long_text"/>5758 </LinearLayout>59 </ScrollView>6061 <FrameLayout62 android:id="@+id/bottom_sheet"63 android:layout_width="match_parent"64 android:layout_height="wrap_content"65 app:behavior_hideable="true"66 app:layout_behavior="android.support.design.widget.BottomSheetBehavior">6768 <Button69 android:id="@+id/bottom_button"70 android:layout_width="match_parent"71 android:layout_height="50dp"72 android:background="@color/colorPrimary"73 android:text="@string/understood"74 android:textAppearance="@style/rounded_button_text"/>75 </FrameLayout>7677</android.support.design.widget.CoordinatorLayout>7879
The layout consists of a CoordinatorLayout
that has two children: a ScrollView
with the scrollable content and a FrameLayout
with the bottom button. We can now manually show/hide the bottom sheet by calling
1bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED)2
or
1bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN).2
But of course we don’t want to bring the Bottom Sheet up manually, we want it to appear automatically when the rounded button disappears off the screen. The second tutorial of Android Authority that we have linked above demonstrates in case 4, how we can implement a custom behavior and alter a view’s attributes when another widget goes off the screen.
So we could implement a custom CoordinatorLayout.Behavior
following the tutorial, and, instead of toggling the button’s child, set the Bottom sheet’s state to expanded.
But, wait… then we would have to add this custom CoordinatorLayout.Behavior
to the bottom_sheet
FrameLayout, but this already has a coordinator layout set. And we can’t add two behaviors.
The solution is simple. We just need to extend the BottomSheetBehavior
as in the snippet below: (we don’t even have to resort to a Decorator — simple inheritance works).
1package app.we.go.bottombutton;23import android.content.Context;4import android.support.design.widget.BottomSheetBehavior;5import android.support.design.widget.CoordinatorLayout;6import android.util.AttributeSet;7import android.util.Log;8import android.view.View;9import android.widget.FrameLayout;1011/**12 * {@link BottomSheetBehavior} that shows automatically when the dependency goes out of the screen13 * and hides when it comes back in.14 */1516public class OutOfScreenBottomSheetBehavior extends BottomSheetBehavior<FrameLayout> {1718 private int statusBarHeight;1920 public OutOfScreenBottomSheetBehavior(Context context, AttributeSet attrs) {21 super(context, attrs);2223 int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");24 if (resourceId > 0) {25 statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);26 }27 }2829 @Override30 public boolean layoutDependsOn(CoordinatorLayout parent, FrameLayout child, View dependency) {31 return dependency.getId() == R.id.behavior_dependency;32 }3334 @Override35 public boolean onDependentViewChanged(CoordinatorLayout parent, FrameLayout child, View dependency) {36 int[] dependencyLocation = new int[2];3738 dependency.getLocationInWindow(dependencyLocation);39 Log.d("BEHAVIOR", "Location: " + dependencyLocation[1]);4041 if (dependencyLocation[1] <= statusBarHeight) {42 if (getState() != STATE_EXPANDED) {43 setState(STATE_EXPANDED);44 }45 } else {46 setState(STATE_HIDDEN);47 }48 return false;49 }5051}52
The implementation is really simple, we just get the position of the coordinator’s dependency, and as soon as it hides behind the status bar, we set the state to STATE_EXPANDED
by calling the parent’s setState
method.
A few notes about the code above:
- The code uses a somewhat unsafe method of calculating the status bar’s height. There are countless questions on StackOverflow about how to get the status bar’s height. I consider this to be the cleaner one. If you want to avoid it, you might be tempted to just trigger the action when the y-location of the dependency on screen is 0. Keep in mind though that if your status bar is not translucent, the position will never be 0.
- For all this to work, there is a small addition that we need to do to the layout code in order for the whole thing to work.
1<?xml version="1.0" encoding="utf-8"?>2<android.support.design.widget.CoordinatorLayout3 xmlns:android="http://schemas.android.com/apk/res/android"4 xmlns:app="http://schemas.android.com/apk/res-auto"5 android:layout_width="match_parent"6 android:layout_height="match_parent">78 <!-- The Scroll View as before, but we are ommiting it... -->910 <FrameLayout11 android:id="@+id/bottom_sheet"12 android:layout_width="match_parent"13 android:layout_height="wrap_content"14 app:behavior_hideable="true"15 app:layout_behavior="app.we.go.bottombutton.OutOfScreenBottomSheetBehavior">1617 <Button18 android:id="@+id/bottom_button"19 android:layout_width="match_parent"20 android:layout_height="50dp"21 android:background="@color/colorPrimary"22 android:text="@string/understood"23 android:textAppearance="@style/rounded_button_text"/>24 </FrameLayout>2526 <!-- This view serves as the dependency of the of the OutOfScreenBottomSheetBehavior27 (it has to be a direct child of the CoordinatorLayout) -->28 <View29 android:id="@+id/behavior_dependency"30 android:layout_width="0dp"31 android:layout_height="0dp"32 app:layout_anchor="@id/got_it"33 app:layout_anchorGravity="bottom"/>3435</android.support.design.widget.CoordinatorLayout>3637
The CoordinatorLayout only “coordinates” its direct children, so we can’t actually make the behavior directly depend on the rounded_button
. The trick to overcoming this limitation is to add the dummy View behavior_dependency
, which is anchored to the rounded_button
and so moves in sync with it, and then use this view as the dependency view in our Custom behavior. Check out the full source code from this Android tutorial on Github.
If you are looking for more content for Android developers, we created an article about the most common Android app architectures and which one Google recommends. Also, we analyze the advantages of using Kotlin vs Java for Android development.