Implementing Stretchable Bottom Sheets and Collapsing Toolbars in Android
In Android development, creating interactive bottom panels and collapsible interfaces enhances user experience significantly. This article explores two powerful components: BottomSheetBehavior for stretchable bottom sheets and AppBarLayout for collapsing toolbar patterns.
BottomSheetBehavior Implementation
The BottomSheetBehavior from Google's Material Design library enables views to function as draggable bottom panels within a CoordinatorLayout. This is particularly useful when combined with scrolling content like RecyclerView.
XML Layout Configuration
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/btn_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/icon_back"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="12dp"
android:layout_marginStart="15dp"
android:padding="5dp"/>
<TextView
android:id="@+id/txt_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Content Details"
android:textColor="#ff000000"
android:textSize="16sp"
app:layout_constraintTop_toTopOf="@+id/btn_back"
app:layout_constraintBottom_toBottomOf="@+id/btn_back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/img_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/btn_back"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="45dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/sliding_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
app:behavior_peekHeight="200dp"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="73dp"
android:elevation="0.5dp"
android:background="@drawable/bg_action_container"
android:layout_marginHorizontal="20dp">
<TextView
android:id="@+id/btn_favorite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Favorite"
android:textSize="10sp"
android:drawableTop="@drawable/selector_star"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_preview"
app:layout_constraintHorizontal_chainStyle="spread"/>
<TextView
android:id="@+id/btn_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Preview"
android:textSize="10sp"
android:drawableTop="@mipmap/icon_preview"
app:layout_constraintTop_toTopOf="@+id/btn_favorite"
app:layout_constraintStart_toEndOf="@+id/btn_favorite"
app:layout_constraintEnd_toStartOf="@+id/btn_share" />
<TextView
android:id="@+id/btn_share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Share"
android:textSize="10sp"
android:drawableTop="@mipmap/icon_share"
app:layout_constraintTop_toTopOf="@+id/btn_favorite"
app:layout_constraintStart_toEndOf="@+id/btn_preview"
app:layout_constraintEnd_toStartOf="@+id/btn_download"/>
<TextView
android:id="@+id/btn_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Save"
android:textSize="10sp"
android:drawableTop="@mipmap/icon_download"
app:layout_constraintTop_toTopOf="@+id/btn_favorite"
app:layout_constraintStart_toEndOf="@+id/btn_share"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_panel_background"
android:elevation="0.5dp"
android:layout_marginTop="20dp"
android:orientation="vertical">
<View
android:layout_width="105dp"
android:layout_height="5dp"
android:layout_gravity="center"
android:background="#333333"
android:layout_marginTop="10dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="More Recommendations"
android:textColor="#ff333333"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginStart="20dp"
android:layout_marginTop="15dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_similar"
android:layout_width="match_parent"
android:layout_height="300dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3"
android:layout_marginTop="5dp">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Key Configuration Attributes
- behavior_peekHeight: Sets the collapsed height (default state). Use
setPeekHeight()in code. - behavior_hideable: Enables complete hiding when swiping down. Use
setHideable(true/false). - behavior_skipCollapsed: Skips the collapsed state when expanding/collapsing. Use
setSkipCollapsed(true/false).
Java Implementation with State Callbacks
View slidingPanel = findViewById(R.id.sliding_panel);
BottomSheetBehavior panelBehavior = BottomSheetBehavior.from(slidingPanel);
panelBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, @BottomSheetBehavior.State int newState) {
String stateDescription;
switch (newState) {
case BottomSheetBehavior.STATE_DRAGGING:
stateDescription = "STATE_DRAGGING";
break;
case BottomSheetBehavior.STATE_SETTLING:
stateDescription = "STATE_SETTLING";
break;
case BottomSheetBehavior.STATE_EXPANDED:
stateDescription = "STATE_EXPANDED";
break;
case BottomSheetBehavior.STATE_COLLAPSED:
stateDescription = "STATE_COLLAPSED";
break;
case BottomSheetBehavior.STATE_HIDDEN:
stateDescription = "STATE_HIDDEN";
break;
default:
stateDescription = "STATE_UNKNOWN";
break;
}
Log.d("PanelDemo", "Current state: " + stateDescription);
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
Log.d("PanelDemo", "Slide offset: " + slideOffset);
}
});
Collapsing Toolbar Layout
The collapsing toolbar pattern uses AppBarLayout combined with CoordinatorLayout to create smooth collapsing effects as users scroll content.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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:orientation="vertical">
<com.dokiwei.lib_base.widget.TitleBar
android:id="@+id/custom_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:tb_left_icon="@mipmap/icon_return"
app:tb_title="Invitation Script"
app:tb_title_size="16sp"
app:tb_title_style="bold" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#00000000"
app:elevation="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="10dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<FrameLayout
android:id="@+id/advertisement_slot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:layout_marginTop="10dp"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<com.allen.library.shape.ShapeConstraintLayout
android:id="@+id/content_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:layout_marginHorizontal="16dp"
android:paddingHorizontal="12dp"
android:paddingTop="20dp"
app:layout_constraintTop_toBottomOf="@+id/v"
app:layout_constraintBottom_toBottomOf="parent"
app:shapeCornersTopLeftRadius="10dp"
app:shapeCornersTopRightRadius="10dp"
app:shapeSolidColor="#ffffff">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintHeight_percent="0.98"
app:layout_constraintBottom_toBottomOf="parent">
<com.dokiwei.lib_base.widget.ProgressWebview
android:id="@+id/web_content"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>
</com.allen.library.shape.ShapeConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
Scroll Flags Explained
The layout_scrollFlags attribute controls how the AppBarLayout responds to scrolling:
- scroll: Enables scrolling behavior; required for any colalpsing functionality
- exitUntilCollapsed: Maintains minimum height (minHeight) even when fully collapsed
- enterAlways: Displays the toolbar immediately when scrolling up
- snap: Snaps to either fully collapsed or fully expanded states based on scroll distance
These components work together seamlessly within the CoordinatorLayout to create polished, interactive UI patterns that adapt fluidly to user interactions.