Bottom Sheet is a really nice way to create improved UI and UX designs. They are used by almost all big applications out there, be it Uber, Google Maps, Google Docs and almost any other Google application. Android comes with built in support for making bottom sheets. We can make bottom sheets in two ways, we are going to see both in this tutorial.

The complete working code is available at this GitHub repo.

Persistent Bottom Sheet

Persistent stays always in your layout available to you all the time. When you want your user to move the sheet up and down manually then you need to use persistent bottom sheet. Persistent bottom sheet works with CoordinatorLayout. Lets see an example.

Below is a layout file containing a LinearLayout with two Buttons as the main layout, initially visible to the user, followed by a NestedScrollView which acts as the bottom sheet.

Bottom sheet xml layout <?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" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.thetechnocafe.gurleensethi.bottomsheets.BottomSheetActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <Button android:id="@+id/openBottomSheetButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Open Bottom Sheet" /> <Button android:id="@+id/closeBottomSheetButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Close Bottom Sheet" /> </LinearLayout> <android.support.v4.widget.NestedScrollView android:id="@+id/nestedScrollView" android:layout_width="match_parent" android:layout_height="300dp" android:background="@android:color/holo_green_light" android:clipToPadding="true" app:behavior_hideable="true" app:behavior_peekHeight="0dp" app:layout_behavior="@string/bottom_sheet_behavior"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/random_title" android:textColor="@android:color/white" android:textSize="24sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="@string/lorem_ipsum" android:textColor="@android:color/white" /> </LinearLayout> </android.support.v4.widget.NestedScrollView> </android.support.design.widget.CoordinatorLayout> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 <? 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" xmlns : tools = "http://schemas.android.com/tools" android : layout_width = "match_parent" android : layout_height = "match_parent" tools : context = "com.thetechnocafe.gurleensethi.bottomsheets.BottomSheetActivity" > < LinearLayout android : layout_width = "match_parent" android : layout_height = "wrap_content" android : orientation = "vertical" app : layout_behavior = "@string/appbar_scrolling_view_behavior" > < Button android : id = "@+id/openBottomSheetButton" android : layout_width = "match_parent" android : layout_height = "wrap_content" android : text = "Open Bottom Sheet" / > < Button android : id = "@+id/closeBottomSheetButton" android : layout_width = "match_parent" android : layout_height = "wrap_content" android : text = "Close Bottom Sheet" / > < / LinearLayout > < android . support . v4 . widget . NestedScrollView android : id = "@+id/nestedScrollView" android : layout_width = "match_parent" android : layout_height = "300dp" android : background = "@android:color/holo_green_light" android : clipToPadding = "true" app : behavior_hideable = "true" app : behavior_peekHeight = "0dp" app : layout_behavior = "@string/bottom_sheet_behavior" > < LinearLayout android : layout_width = "match_parent" android : layout_height = "match_parent" android : orientation = "vertical" android : padding = "16dp" > < TextView android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : text = "@string/random_title" android : textColor = "@android:color/white" android : textSize = "24sp" / > < TextView android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : layout_marginTop = "16dp" android : text = "@string/lorem_ipsum" android : textColor = "@android:color/white" / > < / LinearLayout > < / android . support . v4 . widget . NestedScrollView > < / android . support . design . widget . CoordinatorLayout >

The attribute app:behavior_hideable=”true” makes the bottom sheets completely hide-able even if you have set a peek height. Remove this attribute (or set it to false) if you want the bottom sheet to stay at least at a give peek height.

To make a layout behave as BottomSheet, you need to add the app:layout_behaviour=”@string/bottom_sheet_behaviour” to the layout (as added to the NestedScrollView in above case. Line 37).

Sometimes even after adding the behaviour, bottom sheet will not hide and will keep showing, in this case you will explicitly have to set the peek height of the bottom sheet. To do this just add the following line.

app:behavior_peekHeight="0dp" 1 app : behavior_peekHeight = "0dp"

This will set the peek height of the bottom sheet. Increase the value to see how the bottom sheets comes up.

Referencing Bottom Sheet in activity

To control the bottom sheet from you activity you need to get a reference of it. Here is how to get the reference.

Reference from activity public class BottomSheetActivity extends AppCompatActivity { private BottomSheetBehavior mBottomSheetBehaviour; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_bottom_sheet); View nestedScrollView = (View) findViewById(R.id.nestedScrollView); mBottomSheetBehaviour = BottomSheetBehavior.from(nestedScrollView); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 public class BottomSheetActivity extends AppCompatActivity { private BottomSheetBehavior mBottomSheetBehaviour ; @ Override protected void onCreate ( Bundle savedInstanceState ) { super . onCreate ( savedInstanceState ) ; setContentView ( R . layout . activity_bottom_sheet ) ; View nestedScrollView = ( View ) findViewById ( R . id . nestedScrollView ) ; mBottomSheetBehaviour = BottomSheetBehavior . from ( nestedScrollView ) ; } }

The highlighted lines show the main code required to get the reference. First you get the reference to the view that has the bottom sheet behaviour (NestedScrollView in our case) tag and then get the BottomSheetBehaviour instance using BottomSheetBehaviour.from(view).

In the activity you can programmatically set the peek height as well the state of bottom sheet.

Setting peek height and state programmatically mBottomSheetBehaviour.setPeekHeight(200); //Set the peek height mBottomSheetBehaviour.setState(BottomSheetBehavior.STATE_EXPANDED); // Will show the bottom sheet mBottomSheetBehaviour.setState(BottomSheetBehavior.STATE_COLLAPSED); // Will hide the bottom sheet 1 2 3 4 mBottomSheetBehaviour . setPeekHeight ( 200 ) ; //Set the peek height mBottomSheetBehaviour . setState ( BottomSheetBehavior . STATE_EXPANDED ) ; // Will show the bottom sheet mBottomSheetBehaviour . setState ( BottomSheetBehavior . STATE_COLLAPSED ) ; // Will hide the bottom sheet

Listening to state changes

Bottom sheets define 5 different states, you can listen to state changes by setting a callback function.

Listening to state changes mBottomSheetBehaviour.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { String state = ""; switch (newState) { case BottomSheetBehavior.STATE_DRAGGING: { state = "DRAGGING"; break; } case BottomSheetBehavior.STATE_SETTLING: { state = "SETTLING"; break; } case BottomSheetBehavior.STATE_EXPANDED: { state = "EXPANDED"; break; } case BottomSheetBehavior.STATE_COLLAPSED: { state = "COLLAPSED"; break; } case BottomSheetBehavior.STATE_HIDDEN: { state = "HIDDEN"; break; } } Toast.makeText(BottomSheetActivity.this, "Bottom Sheet State Changed to: " + state, Toast.LENGTH_SHORT).show(); } @Override public void onSlide(@NonNull View bottomSheet, float slideOffset) { } }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 mBottomSheetBehaviour . setBottomSheetCallback ( new BottomSheetBehavior . BottomSheetCallback ( ) { @ Override public void onStateChanged ( @ NonNull View bottomSheet , int newState ) { String state = "" ; switch ( newState ) { case BottomSheetBehavior . STATE_DRAGGING : { state = "DRAGGING" ; break ; } case BottomSheetBehavior . STATE_SETTLING : { state = "SETTLING" ; break ; } case BottomSheetBehavior . STATE_EXPANDED : { state = "EXPANDED" ; break ; } case BottomSheetBehavior . STATE_COLLAPSED : { state = "COLLAPSED" ; break ; } case BottomSheetBehavior . STATE_HIDDEN : { state = "HIDDEN" ; break ; } } Toast . makeText ( BottomSheetActivity . this , "Bottom Sheet State Changed to: " + state , Toast . LENGTH_SHORT ) . show ( ) ; } @ Override public void onSlide ( @ NonNull View bottomSheet , float slideOffset ) { } } ) ;

Bottom Sheet Fragments (Modal)

If you want to show different type of bottom sheets in the same activity or want to make reusable bottom sheet the should use BottomSheetDialogFragment. The process for this is very similar to that of making a DialogFragment. As in any DialogFragment, you will first have to create a layout file.

View for DialogFragment <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="16dp" android:text="Total Price: Rs. 457" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Please select a payment method:" android:textColor="@android:color/black" android:textSize="16sp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:orientation="horizontal"> <Button android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginRight="8dp" android:layout_weight="1" android:background="@android:color/black" android:text="Cash" android:textColor="@android:color/white" /> <Button android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="8dp" android:layout_weight="1" android:background="@android:color/black" android:text="Card" android:textColor="@android:color/white" /> </LinearLayout> </LinearLayout> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <? xml version = "1.0" encoding = "utf-8" ?> < LinearLayout xmlns : android = "http://schemas.android.com/apk/res/android" android : layout_width = "match_parent" android : layout_height = "wrap_content" android : orientation = "vertical" android : padding = "16dp" > < TextView android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : layout_marginBottom = "16dp" android : text = "Total Price: Rs. 457" / > < TextView android : layout_width = "wrap_content" android : layout_height = "wrap_content" android : text = "Please select a payment method:" android : textColor = "@android:color/black" android : textSize = "16sp" / > < LinearLayout android : layout_width = "match_parent" android : layout_height = "wrap_content" android : layout_marginTop = "16dp" android : orientation = "horizontal" > < Button android : layout_width = "0dp" android : layout_height = "wrap_content" android : layout_marginRight = "8dp" android : layout_weight = "1" android : background = "@android:color/black" android : text = "Cash" android : textColor = "@android:color/white" / > < Button android : layout_width = "0dp" android : layout_height = "wrap_content" android : layout_marginLeft = "8dp" android : layout_weight = "1" android : background = "@android:color/black" android : text = "Card" android : textColor = "@android:color/white" / > < / LinearLayout > < / LinearLayout >

Now comes the JAVA file. For making a modal type of bottom sheet, you will have to extend BottomSheetDialogFragment and set the custom view.

BottomSheetFragment public class BottomSheetFragment extends BottomSheetDialogFragment { @Override public void setupDialog(Dialog dialog, int style) { super.setupDialog(dialog, style); //Set the custom view View view = LayoutInflater.from(getContext()).inflate(R.layout.fragment_bottom_sheet, null); dialog.setContentView(view); } } 1 2 3 4 5 6 7 8 9 10 11 public class BottomSheetFragment extends BottomSheetDialogFragment { @ Override public void setupDialog ( Dialog dialog , int style ) { super . setupDialog ( dialog , style ) ; //Set the custom view View view = LayoutInflater . from ( getContext ( ) ) . inflate ( R . layout . fragment_bottom_sheet , null ) ; dialog . setContentView ( view ) ; } }

Now all you have to do is get the instance and show the dialog.

BottomSheetFragment fragment = new BottomSheetFragment(); fragment.show(getSupportFragmentManager(), "TAG"); 1 2 BottomSheetFragment fragment = new BottomSheetFragment ( ) ; fragment . show ( getSupportFragmentManager ( ) , "TAG" ) ;

Listening to state changes in BottomSheetDialogFragment

To listen for state changes in DialogFragment you will first have to get access to the BottomSheetBehaviour instance. Here is how you do it.

Add state listener in BottomSheetDialogFragment public class BottomSheetFragment extends BottomSheetDialogFragment { @Override public void setupDialog(Dialog dialog, int style) { super.setupDialog(dialog, style); //Set the custom view View view = LayoutInflater.from(getContext()).inflate(R.layout.fragment_bottom_sheet, null); dialog.setContentView(view); CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) ((View) view.getParent()).getLayoutParams(); CoordinatorLayout.Behavior behavior = params.getBehavior(); if (behavior != null && behavior instanceof BottomSheetBehavior) { ((BottomSheetBehavior) behavior).setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { String state = ""; switch (newState) { case BottomSheetBehavior.STATE_DRAGGING: { state = "DRAGGING"; break; } case BottomSheetBehavior.STATE_SETTLING: { state = "SETTLING"; break; } case BottomSheetBehavior.STATE_EXPANDED: { state = "EXPANDED"; break; } case BottomSheetBehavior.STATE_COLLAPSED: { state = "COLLAPSED"; break; } case BottomSheetBehavior.STATE_HIDDEN: { dismiss(); state = "HIDDEN"; break; } } Toast.makeText(getContext(), "Bottom Sheet State Changed to: " + state, Toast.LENGTH_SHORT).show(); } @Override public void onSlide(@NonNull View bottomSheet, float slideOffset) { } }); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class BottomSheetFragment extends BottomSheetDialogFragment { @ Override public void setupDialog ( Dialog dialog , int style ) { super . setupDialog ( dialog , style ) ; //Set the custom view View view = LayoutInflater . from ( getContext ( ) ) . inflate ( R . layout . fragment_bottom_sheet , null ) ; dialog . setContentView ( view ) ; CoordinatorLayout . LayoutParams params = ( CoordinatorLayout . LayoutParams ) ( ( View ) view . getParent ( ) ) . getLayoutParams ( ) ; CoordinatorLayout . Behavior behavior = params . getBehavior ( ) ; if ( behavior != null && behavior instanceof BottomSheetBehavior ) { ( ( BottomSheetBehavior ) behavior ) . setBottomSheetCallback ( new BottomSheetBehavior . BottomSheetCallback ( ) { @ Override public void onStateChanged ( @ NonNull View bottomSheet , int newState ) { String state = "" ; switch ( newState ) { case BottomSheetBehavior . STATE_DRAGGING : { state = "DRAGGING" ; break ; } case BottomSheetBehavior . STATE_SETTLING : { state = "SETTLING" ; break ; } case BottomSheetBehavior . STATE_EXPANDED : { state = "EXPANDED" ; break ; } case BottomSheetBehavior . STATE_COLLAPSED : { state = "COLLAPSED" ; break ; } case BottomSheetBehavior . STATE_HIDDEN : { dismiss ( ) ; state = "HIDDEN" ; break ; } } Toast . makeText ( getContext ( ) , "Bottom Sheet State Changed to: " + state , Toast . LENGTH_SHORT ) . show ( ) ; } @ Override public void onSlide ( @ NonNull View bottomSheet , float slideOffset ) { } } ) ; } } }

Make sure to dismiss the dialog when the state is STATE_HIDDEN, the bottom sheet might have disappeared (gone into hidden state) but since this is a DialogFragment, the dialog itself is still visible to the user.

This is how the BottomSheetDialogFragment looks:

The complete working code is available at this GitHub repo.

That is it for this tutorial, hope you have enjoyed it!

Here are some other articles that you might like:

Adding 360 Photo Viewer in your Android App

Using Fonts in Android

Kotlin Data Class Tutorial

How to hide Floating Action Button when scrolling in Recycler View