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:
So, the first step is quite straightforward: add the button at the bottom as a Bottom Sheet.
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:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
android:id="@+id/c"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="70dp"
android:paddingLeft="32dp"
android:paddingRight="32dp"
android:paddingTop="20dp">
android:layout_marginTop="16dp"
android:text="@string/privacy_header_1"
style="@style/header"/>
style="@style/privacy_text"
android:layout_marginTop="29dp"
android:text="@string/privacy_text"/>
android:id="@+id/got_it"
android:layout_width="160dp"
android:layout_height="50dp"
android:layout_marginTop="29dp"
android:text="@string/understood"
android:background="@drawable/rounded_button"
android:textAppearance="@style/rounded_button_text"/>
style="@style/header"
android:layout_marginTop="48dp"
android:text="@string/privacy_header_2"/>
android:id="@+id/long_long_text"
style="@style/privacy_text"
android:layout_marginTop="23dp"
android:text="@string/long_text"/>
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:behavior_hideable="true"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
android:id="@+id/bottom_button"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:text="@string/understood"
android:textAppearance="@style/rounded_button_text"/>
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
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED)
or
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
.
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).
package app.we.go.bottombutton;
import android.content.Context;
import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
/**
* {@link BottomSheetBehavior} that shows automatically when the dependency goes out of the screen
* and hides when it comes back in.
*/
public class OutOfScreenBottomSheetBehavior extends BottomSheetBehavior{
private int statusBarHeight;
public OutOfScreenBottomSheetBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
}
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, FrameLayout child, View dependency) {
return dependency.getId() == R.id.behavior_dependency;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FrameLayout child, View dependency) {
int[] dependencyLocation = new int[2];
dependency.getLocationInWindow(dependencyLocation);
Log.d("BEHAVIOR", "Location: " + dependencyLocation[1]);
if (dependencyLocation[1] <= statusBarHeight) {
if (getState() != STATE_EXPANDED) {
setState(STATE_EXPANDED);
}
} else {
setState(STATE_HIDDEN);
}
return false;
}
}
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.
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:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:behavior_hideable="true"
app:layout_behavior="app.we.go.bottombutton.OutOfScreenBottomSheetBehavior">
android:id="@+id/bottom_button"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:text="@string/understood"
android:textAppearance="@style/rounded_button_text"/>
android:id="@+id/behavior_dependency"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_anchor="@id/got_it"
app:layout_anchorGravity="bottom"/>
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.
Are you searching for your next programming challenge?
Scalable Path is always on the lookout for top-notch talent. Apply today and start working with great clients from around the world!