Android How to implement Bottom Sheet from Material Design docs

How do you implement the bottom sheet specficiation? http://www.google.com/design/spec/components/bottom-sheets.html

The new update to Google Drive shows this with the Floating Action Button press ->

enter image description here

Granted the specs never say anything about rounded corners, regardless it is possible to do, just unsure of how to go about it. Currently using the AppCompat library and target set to 21.

Thanks

79771 次浏览

I would go with a straight corners as it is in the guidelines. As for the implementation - maybe it is best to use the idea from this project: https://github.com/umano/AndroidSlidingUpPanel

I think that you could use it as it is or take the idea for the implementation. Another great article on how to implement similar sliding panel can be found here: http://blog.neteril.org/blog/2013/10/10/framelayout-your-best-ui-friend/

Edit

The BottomSheet is now part of the android-support-library. See John Shelleys' answer.


Unfortunately there's currently no "official" way on how to do this (at least none that I'm aware of).
Luckily there's a library called "BottomSheet" (click) which mimics the look and feel of the BottomSheet and supports Android 2.1 and up.

In case of the Drive app, here's how the code would look like with this library:

    new BottomSheet.Builder(this, R.style.BottomSheet_Dialog)
.title("New")
.grid() // <-- important part
.sheet(R.menu.menu_bottom_sheet)
.listener(new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// TODO
}
}).show();

menu_bottom_sheet (basically a standard /res/menu/*.xml resource)

<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/folder"
android:title="Folder"
android:icon="@drawable/ic_action_folder" />
<item
android:id="@+id/upload"
android:title="Upload"
android:icon="@drawable/ic_action_file_upload" />
<item
android:id="@+id/scan"
android:title="Scan"
android:icon="@drawable/ic_action_camera_alt" />
</menu>

Output looks like this:

picture of the bottom sheet

Which, I think, comes pretty close to the original. If you're not happy with the colors you can customize it - see this (click).

Here are some of the other options :

  • There is one available from Flipboard, however the embedding activity needs to be modified for the bottomsheet to work.
  • tutti-ch's bottomsheet : This has been extracted from Android Repo's ResolverActivity and the launching activity need not be modified.

Answering my own question so developers know that the new support library provides this finally! All hail the all powerful Google!

An example from the Android Developer's Blog:

// The View with the BottomSheetBehavior
View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setBottomSheetCallback(new BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
// React to state change
}


@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
// React to dragging events
}
});

@reVerse's answer above is still a valid option but its nice to know that there is a standard that Google supports too.

Google recently released Android Support Library 23.2 which officially brings Bottom sheets to the Android Design Support Library.

You can now use Official BottomSheetBehavior API from android support library 23.2.

Below is sample code snippet

bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.bottomSheet));


case R.id.expandBottomSheetButton:
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
break;
case R.id.collapseBottomSheetButton:
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
break;
case R.id.hideBottomSheetButton:
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
break;
case R.id.showBottomSheetDialogButton:
new MyBottomSheetDialogFragment().show(getSupportFragmentManager(), "sample");

Following the blog post: http://android-developers.blogspot.com/2016/02/android-support-library-232.html

My xml ended up looking like this:

<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/coordinator_layout"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="100dp"
android:orientation="horizontal"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<ImageView
android:src="@android:drawable/ic_input_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>

And in my onCreateView of my fragment:

    coordinatorLayout = (CoordinatorLayout)v.findViewById(R.id.coordinator_layout);
View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setPeekHeight(100);
behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
// React to state change
}


@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
// React to dragging events
}
});

The default of setPeekHeight is 0, so if you don't set it, you won't be able to see your view.

Bottom Sheet image

If you want to achieve bottom sheet like this, follow this design pattern few simple steps

  1. create bottom_sheet_layout.xml layout file
  2. create bottom_sheet_background.xml drawable file

set your bottom_sheet_background.xml drawable file like this

    <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
    

<solid android:color="@color/bottom_sheet_background"/>
<corners
android:topRightRadius="20dp"
android:topLeftRadius="20dp"/>
    

</shape>
    

    

your bottom_sheet_layout.xml

    <?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/bottom_Sheet"
android:background="@drawable/bottom_sheet_background"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingTop="16dp"
android:paddingBottom="42dp"
android:orientation="vertical"
xmlns:app="http://schemas.android.com/apk/res-auto">
    

<ImageView
android:id="@+id/rectangle_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:background="@drawable/rectangle_39"
/>
    

    

//add your design code here
    

</LinearLayout>
    

    

And your activity_main.xml or fragment

    <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
    

    

//design your code here
    

    

//this is your bottom sheet layout included here
<include
android:id="@+id/bottom_sheet_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.
bottomsheet.BottomSheetBehavior"
layout="@layout/bottom_sheet_layout"/>
    

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Finally, add code in your MainActivity or Fragment class. Here I am adding Kotlin code inside your onCreate or onCreateView

BottomSheetBehavior.from(binding.bottomSheetLayout.bottomSheet).apply {
//peek height is default visible height
peekHeight = 200
this.state = BottomSheetBehavior.STATE_COLLAPSED
}

That's it!

Now with Android Jetpack Compose released which is Android's modern UI toolkit , Bottomsheets can be made more easily without using any xml code:-

1.To create Persistent bottom sheet where user can access the content outside of the bottom sheet’s scope:-

enter image description here

val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
MaterialTheme {
Column {
BottomSheetScaffold(
modifier = Modifier.fillMaxSize(),
topBar = { TopAppBar(viewModel, onNavigateToRecipeListScreen, hideKeyBoard) },
content = {
CreateRecipeContent(
viewModel,
context,
readExternalStorage,
bottomSheetScaffoldState,
coroutineScope
)
},
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Column(
Modifier
.fillMaxWidth()
.height(200.dp)
.background(color = colorResource(id = R.color.colorPrimaryLight))
)
{
Text(
text = "SELECT PICTURE",
style = TextStyle(fontSize = 26.sp),
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(8.dp)
.align(Alignment.Start),
color = Color.Black
)
Spacer(modifier = Modifier.height(16.dp))
IconButton(onClick = {
when {
context.let { it1 ->
ContextCompat.checkSelfPermission(
it1,
Manifest.permission.READ_EXTERNAL_STORAGE
)
} == PackageManager.PERMISSION_GRANTED -> {
val takePictureIntent =
Intent(MediaStore.ACTION_IMAGE_CAPTURE)
launchCamera(takePictureIntent)
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
else -> {
// You can directly ask for the permission.
// The registered ActivityResultCallback gets the result of this request.
viewModel.isCameraPermissionAsked = true
readExternalStorage()
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
}


}, modifier = Modifier.fillMaxWidth()) {
Text(
text = "TAKE PHOTO",
style = TextStyle(fontSize = 20.sp),
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(8.dp)
.align(Alignment.Start),
textAlign = TextAlign.Left,
color = Color.Black
)
}
Spacer(modifier = Modifier.height(16.dp))
IconButton(onClick = {
when {
context.let { it1 ->
ContextCompat.checkSelfPermission(
it1,
Manifest.permission.READ_EXTERNAL_STORAGE
)
} == PackageManager.PERMISSION_GRANTED -> {
val galleryIntent = Intent(
Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
)
galleryIntent.type = "image/*"
launchGalley(galleryIntent)
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
else -> {
// You can directly ask for the permission.
// The registered ActivityResultCallback gets the result of this request.
viewModel.isCameraPermissionAsked = false
readExternalStorage()
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
}


}, modifier = Modifier.fillMaxWidth()) {
Text(
text = "CHOOSE FROM GALLERY",
style = TextStyle(fontSize = 20.sp),
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(8.dp)
.align(Alignment.Start),
textAlign = TextAlign.Left,
color = Color.Black
)
}


}
}, sheetPeekHeight = 0.dp
)




}
}

Above code snipped and screenshot is from the app:-

https://play.google.com/store/apps/details?id=com.bhuvnesh.diary

created entirely using Jetpack Compose by me

  1. To create Modal Bottom Sheet where users cannot access the content out of the bottom sheet’s scope:-

     ModalBottomSheetLayout(
    sheetState = modalBottomSheetState,
    sheetElevation = 8.dp,
    sheetContent = {
    //sheet content
    }
    ) {
    ...
    //main content
    }