Creating Bottom Sheet with Material Design in Android

    The Design Support Library provides implementations of many patterns of Material Design, also new animations and effects. When version 23.2.0 released, it adds a few new support libraries as well as new features to many of the existing libraries, developers were give another missing component from the Material Design Guidelines, for example: this release allows developers to easily add a bottom sheet to their app without using any external library!
     More details, a bottom sheet is a sheet that slides up from the bottom edge of the screen. Bottom sheets are displayed only as a result of a user-initiated action, and can be swiped up to reveal additional content. A bottom sheet can be a temporary modal surface or a persistent structural element of an app. In this post, with using  android.support.design.widget.BottomSheetBehavior, I will make an output like this DEMO VIDEO:


Import Design support library

    You must import version 23.2.0  or later to app/build.gradle before start coding:
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.2.0'
    compile 'com.android.support:design:23.2.0'
}

Designing layout in XML

    By attaching a BottomSheetBehavior as a child view of a CoordinatorLayout, adding app:layout_behavior="@string/bottom_sheet_behavior", you’ll automatically get the appropriate touch detection to transition between five state:
  • STATE_COLLAPSED: this collapsed state is the default and shows just a portion of the layout along the bottom. The height can be controlled with the app:behavior_peekHeight attribute (defaults to 0).
  • STATE_DRAGGING: the intermediate state while the user is directly dragging the bottom sheet up or down
  • STATE_SETTLING: that brief time between when the View is released and settling into its final position.
  • STATE_EXPANDED: the fully expanded state of the bottom sheet, where either the whole bottom sheet is visible (if its height is less than the containing CoordinatorLayout) or the entire CoordinatorLayout is filled.
  • STATE_HIDDEN: disabled by default (and enabled with the app:behavior_hideable attribute), enabling this allows users to swipe down on the bottom sheet to completely hide the bottom sheet.
XML layout will be like this (in this example, the bottom sheet is a GridView):
activity_main.xml
<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"
    android:fitsSystemWindows="true"
    tools:context="com.example.bottomsheetbehavior.MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/AppTheme.AppBarOverlay">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:popupTheme="@style/AppTheme.PopupOverlay" />

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

        <ListView
            android:id="@+id/list_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <GridView
        android:id="@+id/bottom_sheet"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:numColumns="3"
        app:behavior_hideable="true"
        android:background="#dddddd"
        app:layout_behavior="@string/bottom_sheet_behavior" />

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

Setting up in programmatically code

    Bottom sheet comes with implementations BottomSheetBehavior. It has a really handy method from(view) that is used to take the instance of the behavior from the layout params, of course if they are of same type. Moreover, the BottomSheetBehavior can pin  BottomSheetCallback to receive callbacks like state changes and offset changes for your sheet. Setting up process done by this code:
bottomSheet = (GridView) findViewById(R.id.bottom_sheet);
        bottomSheet.setTranslationY(getStatusBarHeight());
        BottomSheetBehavior sheetBehavior = BottomSheetBehavior.from(bottomSheet);
        sheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
            boolean first = true;

            @Override
            public void onStateChanged(View bottomSheet, int newState) {
                Log.d("MainActivity", "onStateChanged: " + newState);
                //TODO: add more code here
            }

            @Override
            public void onSlide(View bottomSheet, float slideOffset) {
                Log.d("MainActivity", "onSlide: ");
                if (first) {
                    first = false;
                    bottomSheet.setTranslationY(0);
                }
            }
        });

Full Code of the project

    In this project, the main layout is a ListView, after click at any row, the bottom sheet will be displayed. Create an adapter for it first: BooksAdapter.java

package info.devexchanges.bottomsheetmd;

import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import java.util.List;

public class BooksAdapter extends ArrayAdapter<Book> {

    private List<Book> books;
    private Activity activity;

    public BooksAdapter(Activity context, int resource, List<Book> objects) {
        super(context, resource, objects);
        this.activity = context;
        this.books = objects;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        if (convertView == null) {
            LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.item_list_view, null, false);
        } else {
            convertView.getTag();
        }

        TextView name = (TextView) convertView.findViewById(R.id.book_name);
        TextView author = (TextView) convertView.findViewById(R.id.book_author);

        name.setText("Book name: " + getItem(position).getName());
        author.setText("Author: " + getItem(position).getAuthor());

        return convertView;
    }
}
    Layout for each ListView item:
item_listview.xml
<?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="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/book_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="@dimen/activity_horizontal_margin"
        android:textColor="@android:color/holo_green_dark"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/book_author"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/activity_horizontal_margin"
        android:textColor="@android:color/holo_blue_dark" />

</LinearLayout>
    As note above, the bottom sheet is a GridView, some "action button" were placed here, this is it's adapter (based on ArrayAdapter, too):
BottomSheetAdapter.java
package info.devexchanges.bottomsheetmd;

import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class BottomSheetAdapter extends ArrayAdapter<Integer> {

    private Activity activity;

    public BottomSheetAdapter(Activity context, int resource, Integer[] objects) {
        super(context, resource, objects);
        this.activity = context;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View row = convertView;
        ViewHolder holder;

        if (row == null) {
            LayoutInflater inflater = activity.getLayoutInflater();
            row = inflater.inflate(R.layout.item_grid, parent, false);

            holder = new ViewHolder();
            holder.image = (ImageView) row.findViewById(R.id.image);
            row.setTag(holder);
        } else {
            holder = (ViewHolder) row.getTag();
        }

        holder.image.setImageResource(getItem(position));

        return row;
    }

    private static class ViewHolder {
        ImageView image;
    }
}
    And it's each item layout (only an ImageView):
item_grid.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_centerInParent="true"
        android:contentDescription="@string/app_name"
        android:padding="10dp" />

</RelativeLayout>
    The most important file is the main activity. Handling each ListView and bottom sheet(GridView) item click event here:
MainActivity.java

package info.devexchanges.bottomsheetmd;

import android.content.DialogInterface;
import android.os.Bundle;
import android.support.design.widget.BottomSheetBehavior;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    private ListView listView;
    private GridView bottomSheet;
    private ArrayAdapter<Integer> bottomSheetAdapter;
    private Toolbar toolbar;
    private ArrayAdapter<Book> adapter;
    private ArrayList<Book> books;
    private Book selectedBook;
    private BottomSheetBehavior sheetBehavior;
    private Integer[] bottomItems = {R.drawable.add, R.drawable.mail, R.drawable.delete, R.drawable.facebook,
            R.drawable.google_plus, R.drawable.twitter};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        createBooksData();

        listView = (ListView) findViewById(R.id.list_view);
        toolbar = (Toolbar) findViewById(R.id.toolbar);

        setSupportActionBar(toolbar);

        //set main ListView adapter
        adapter = new BooksAdapter(this, R.layout.item_list_view, books);
        listView.setAdapter(adapter);

        //set bottom sheet(GridView) adapter
        bottomSheetAdapter = new BottomSheetAdapter(this, R.layout.item_grid, bottomItems);
        bottomSheet.setAdapter(bottomSheetAdapter);

        bottomSheet = (GridView) findViewById(R.id.bottom_sheet);
        bottomSheet.setTranslationY(getStatusBarHeight());
        sheetBehavior = BottomSheetBehavior.from(bottomSheet);
        sheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
            boolean first = true;

            @Override
            public void onStateChanged(View bottomSheet, int newState) {
                Log.d("MainActivity", "onStateChanged: " + newState);
            }

            @Override
            public void onSlide(View bottomSheet, float slideOffset) {
                Log.d("MainActivity", "onSlide: ");
                if (first) {
                    first = false;
                    bottomSheet.setTranslationY(0);
                }
            }
        });

        //main listview item click event
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                selectedBook = (Book) parent.getAdapter().getItem(position);
                sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
            }
        });

        //bottom sheet item click event
        bottomSheet.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
                if (position == 2) {
                    //delete an item in listview
                    new AlertDialog.Builder(MainActivity.this)
                            .setTitle("Delete Confirm")
                            .setMessage("Are you sure you want to delete " + "\"" + selectedBook.getName().toUpperCase() + "\"" + "?")
                            .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog, int which) {
                                    //delete it
                                    books.remove(selectedBook);
                                    adapter.notifyDataSetChanged();
                                    sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                                }
                            })
                            .setNegativeButton(android.R.string.no, null)
                            .setIcon(android.R.drawable.ic_dialog_alert)
                            .show();
                } else if (position == 0) {

                    //add new book to main ListView
                    AlertDialog.Builder alertBuilder = new AlertDialog.Builder(MainActivity.this);
                    alertBuilder.setTitle("Adding a book");
                    alertBuilder.setMessage("Input book name and author");

                    LinearLayout layout = new LinearLayout(MainActivity.this);
                    layout.setOrientation(LinearLayout.VERTICAL);

                    final EditText name = new EditText(MainActivity.this);
                    name.setHint("Book title");
                    layout.addView(name);

                    final EditText author = new EditText(MainActivity.this);
                    author.setHint("Book's author");
                    layout.addView(author);

                    alertBuilder.setView(layout);

                    alertBuilder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            //add this item
                            books.add(new Book(name.getText().toString(), author.getText().toString()));
                            adapter.notifyDataSetChanged();
                            sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                        }
                    });
                    alertBuilder.setNegativeButton(android.R.string.no, null);
                    alertBuilder.setIcon(android.R.drawable.ic_dialog_alert);

                    alertBuilder.show();
                } else if (position == 3) {
                    Toast.makeText(getBaseContext(), "Share this on Facebook", Toast.LENGTH_SHORT).show();
                } else if (position == 4) {
                    Toast.makeText(getBaseContext(), "Share this on Google+", Toast.LENGTH_SHORT).show();
                } else if (position == 5) {
                    Toast.makeText(getBaseContext(), "Share this on Twitter", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }

    private void createBooksData() {
        books = new ArrayList<>();
        books.add(new Book("Lão Hạc", "Nam Cao"));
        books.add(new Book("Số đỏ", "Vũ Trọng Phụng"));
        books.add(new Book("Tắt đèn", "Ngô Tất Tố"));
        books.add(new Book("The wild chase sheep", "Haruki Murakami"));
        books.add(new Book("Mảnh trăng cuối rừng", "Nguyễn Minh Châu"));
        books.add(new Book("The Adventures of Huckleberry Finn", "Mark Twain"));
        books.add(new Book("Dế Mèn phiêu lưu ký", "Tô Hoài"));
        books.add(new Book("Never let me go", "Kazuo Ishiguro"));
        books.add(new Book("Harry Potter", "J.K. Rowling"));
        books.add(new Book("The Last Leaf", "O. Henry"));
        books.add(new Book("The Call of the Wild", "Jack London"));
        books.add(new Book("Le Papa de Simon", "Guy de Maupassant"));
        books.add(new Book("One Hundred Years of Solitude", "Gabriel García Márquez"));
    }

    private int getStatusBarHeight() {
        int result = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    @Override
    public void onBackPressed() {
        if (sheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
            sheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
        } else {
            super.onBackPressed();
        }
    }
}
    The POJO of the project:
Book.java
package info.devexchanges.bottomsheetmd;

public class Book {

    private String name;
    private String author;

    public Book(String name, String author) {
        this.name = name;
        this.author = author;
    }

    public String getName() {
        return name;
    }

    public String getAuthor() {
        return author;
    }
}
Make sure that you use "AppCompat no Action Bar theme" with this design:
styles.xml

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Dark" />

</resources>

Running the application

    After running project, click each row of list, the bottom sheet will be shown, it can be dragged to close/collapse:
    Clicking at bottom sheet "Delete button":

    Clicking at the bottom sheet "Add button":

Conclusions

    Through this post, I hope you can learn the way to implement a bottom sheet with Material Design style. Moreover, you can find out some external libraries to create this UI. There are a lot of them that available on @Github can do this trick, for example: BottomSheet or Flipboard bottomsheet. Finally, you can get my project on @Github.



Update: Modal bottom sheet

    There is a type of bottom sheet that popular in a lot of apps called Modal bottom sheet. For example: Google Drive app:
    Please see my updated post to learn the way to create it! :)

Share


Previous post
« Prev Post
Next post
Next Post »