diff --git a/catalog/java/io/material/catalog/bottomsheet/res/layout/cat_bottomsheet_content.xml b/catalog/java/io/material/catalog/bottomsheet/res/layout/cat_bottomsheet_content.xml
index 4b8f640bd3e..6d86bfb9ed6 100644
--- a/catalog/java/io/material/catalog/bottomsheet/res/layout/cat_bottomsheet_content.xml
+++ b/catalog/java/io/material/catalog/bottomsheet/res/layout/cat_bottomsheet_content.xml
@@ -19,6 +19,11 @@
android:id="@+id/bottom_drawer_2"
android:layout_width="match_parent"
android:layout_height="600dp">
+
+
+
+
+
+
+
+
bottomSheetBehavior;
+
+ private boolean accessibilityServiceEnabled;
+ private boolean interactable;
+
+ private final String clickToExpandActionLabel =
+ getResources().getString(R.string.bottomsheet_action_expand);
+ private final String clickToCollapseActionLabel =
+ getResources().getString(R.string.bottomsheet_action_collapse);
+ private final String clickFeedback =
+ getResources().getString(R.string.bottomsheet_drag_handle_clicked);
+
+ private final BottomSheetCallback bottomSheetCallback =
+ new BottomSheetCallback() {
+ @Override
+ public void onStateChanged(
+ @NonNull View bottomSheet, @BottomSheetBehavior.State int newState) {
+ onBottomSheetStateChanged(newState);
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {}
+ };
+
+ public BottomSheetDragHandleView(@NonNull Context context) {
+ this(context, /* attrs= */ null);
+ }
+
+ public BottomSheetDragHandleView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, R.attr.bottomSheetDragHandleStyle);
+ }
+
+ public BottomSheetDragHandleView(
+ @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr);
+
+ // Override the provided context with the wrapped one to prevent it from being used.
+ context = getContext();
+
+ accessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+
+ updateInteractableState();
+
+ ViewCompat.setAccessibilityDelegate(
+ this,
+ new AccessibilityDelegateCompat() {
+ @Override
+ public void onPopulateAccessibilityEvent(View host, @NonNull AccessibilityEvent event) {
+ super.onPopulateAccessibilityEvent(host, event);
+ if (event.getEventType() == TYPE_VIEW_CLICKED) {
+ toggleBottomSheetIfPossible();
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ setBottomSheetBehavior(findParentBottomSheetBehavior());
+ if (accessibilityManager != null) {
+ accessibilityManager.addAccessibilityStateChangeListener(this);
+ onAccessibilityStateChanged(accessibilityManager.isEnabled());
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (accessibilityManager != null) {
+ accessibilityManager.removeAccessibilityStateChangeListener(this);
+ }
+ setBottomSheetBehavior(null);
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ accessibilityServiceEnabled = enabled;
+ updateInteractableState();
+ }
+
+ private void setBottomSheetBehavior(@Nullable BottomSheetBehavior> behavior) {
+ if (bottomSheetBehavior != null) {
+ bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback);
+ }
+ bottomSheetBehavior = behavior;
+ if (bottomSheetBehavior != null) {
+ onBottomSheetStateChanged(bottomSheetBehavior.getState());
+ bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
+ }
+ updateInteractableState();
+ }
+
+ private void onBottomSheetStateChanged(@BottomSheetBehavior.State int state) {
+ String label =
+ state == BottomSheetBehavior.STATE_COLLAPSED
+ ? clickToExpandActionLabel
+ : clickToCollapseActionLabel;
+ ViewCompat.replaceAccessibilityAction(
+ this,
+ AccessibilityActionCompat.ACTION_CLICK,
+ label,
+ (v, args) -> toggleBottomSheetIfPossible());
+ }
+
+ private void updateInteractableState() {
+ interactable = accessibilityServiceEnabled && bottomSheetBehavior != null;
+ ViewCompat.setImportantForAccessibility(
+ this,
+ bottomSheetBehavior != null
+ ? ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES
+ : ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ setClickable(interactable);
+ }
+
+ private boolean toggleBottomSheetIfPossible() {
+ if (!interactable) {
+ return false;
+ }
+ announceAccessibilityEvent(clickFeedback);
+ boolean collapsed = bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED;
+ bottomSheetBehavior.setState(
+ collapsed ? BottomSheetBehavior.STATE_EXPANDED : BottomSheetBehavior.STATE_COLLAPSED);
+ return true;
+ }
+
+ private void announceAccessibilityEvent(String announcement) {
+ if (accessibilityManager == null) {
+ return;
+ }
+ AccessibilityEvent announce =
+ AccessibilityEvent.obtain(AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
+ announce.getText().add(announcement);
+ accessibilityManager.sendAccessibilityEvent(announce);
+ }
+
+ /**
+ * Finds the first ancestor associated with a {@link BottomSheetBehavior}. If none is found,
+ * returns {@code null}.
+ */
+ @Nullable
+ private BottomSheetBehavior> findParentBottomSheetBehavior() {
+ View parent = this;
+ while ((parent = getParentView(parent)) != null) {
+ LayoutParams layoutParams = parent.getLayoutParams();
+ if (layoutParams instanceof CoordinatorLayout.LayoutParams) {
+ CoordinatorLayout.Behavior> behavior =
+ ((CoordinatorLayout.LayoutParams) layoutParams).getBehavior();
+ if (behavior instanceof BottomSheetBehavior) {
+ return (BottomSheetBehavior>) behavior;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static View getParentView(View view) {
+ ViewParent parent = view.getParent();
+ return parent instanceof View ? (View) parent : null;
+ }
+}
diff --git a/lib/java/com/google/android/material/bottomsheet/res-public/values/public.xml b/lib/java/com/google/android/material/bottomsheet/res-public/values/public.xml
index 92f78174922..e00e0bb612d 100644
--- a/lib/java/com/google/android/material/bottomsheet/res-public/values/public.xml
+++ b/lib/java/com/google/android/material/bottomsheet/res-public/values/public.xml
@@ -22,6 +22,7 @@
+
@@ -41,6 +42,7 @@
+
diff --git a/lib/java/com/google/android/material/bottomsheet/res/drawable/mtrl_bottomsheet_drag_handle.xml b/lib/java/com/google/android/material/bottomsheet/res/drawable/mtrl_bottomsheet_drag_handle.xml
new file mode 100644
index 00000000000..a99eb3980be
--- /dev/null
+++ b/lib/java/com/google/android/material/bottomsheet/res/drawable/mtrl_bottomsheet_drag_handle.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/lib/java/com/google/android/material/bottomsheet/res/values/attrs.xml b/lib/java/com/google/android/material/bottomsheet/res/values/attrs.xml
index fc1ac051c64..839d952ae57 100644
--- a/lib/java/com/google/android/material/bottomsheet/res/values/attrs.xml
+++ b/lib/java/com/google/android/material/bottomsheet/res/values/attrs.xml
@@ -22,6 +22,8 @@
+
+
diff --git a/lib/java/com/google/android/material/bottomsheet/res/values/dimens.xml b/lib/java/com/google/android/material/bottomsheet/res/values/dimens.xml
index b0253b4cc34..6be8811d31e 100644
--- a/lib/java/com/google/android/material/bottomsheet/res/values/dimens.xml
+++ b/lib/java/com/google/android/material/bottomsheet/res/values/dimens.xml
@@ -25,4 +25,5 @@
@dimen/m3_sys_elevation_level2
@dimen/m3_sys_elevation_level3
+ 20dp
diff --git a/lib/java/com/google/android/material/bottomsheet/res/values/strings.xml b/lib/java/com/google/android/material/bottomsheet/res/values/strings.xml
index 7b7ed4a155a..d15c9557188 100644
--- a/lib/java/com/google/android/material/bottomsheet/res/values/strings.xml
+++ b/lib/java/com/google/android/material/bottomsheet/res/values/strings.xml
@@ -19,4 +19,9 @@
com.google.android.material.bottomsheet.BottomSheetBehavior
Expand halfway
+
+ Drag handle
+ Expand the bottom sheet
+ Collapse the bottom sheet
+ Drag handle double-tapped
diff --git a/lib/java/com/google/android/material/bottomsheet/res/values/styles.xml b/lib/java/com/google/android/material/bottomsheet/res/values/styles.xml
index ef979dc193c..30ee290c9fa 100644
--- a/lib/java/com/google/android/material/bottomsheet/res/values/styles.xml
+++ b/lib/java/com/google/android/material/bottomsheet/res/values/styles.xml
@@ -80,4 +80,15 @@
+
+
+
diff --git a/lib/java/com/google/android/material/dialog/res/values/themes_base.xml b/lib/java/com/google/android/material/dialog/res/values/themes_base.xml
index 7c50a938151..0e1bba13699 100644
--- a/lib/java/com/google/android/material/dialog/res/values/themes_base.xml
+++ b/lib/java/com/google/android/material/dialog/res/values/themes_base.xml
@@ -78,6 +78,7 @@
- @style/Widget.Material3.Button.TextButton
- @style/Widget.Material3.BottomAppBar
- @style/Widget.Material3.BottomNavigationView
+ - @style/Widget.Material3.BottomSheet.DragHandle
- @style/Widget.Material3.Button.TextButton.Dialog
- @style/Widget.Material3.CompoundButton.CheckBox
- @style/Widget.Material3.Chip.Assist
@@ -312,6 +313,7 @@
- @style/Widget.Material3.Button.TextButton
- @style/Widget.Material3.BottomAppBar
- @style/Widget.Material3.BottomNavigationView
+ - @style/Widget.Material3.BottomSheet.DragHandle
- @style/Widget.Material3.Button.TextButton.Dialog
- @style/Widget.Material3.CompoundButton.CheckBox
- @style/Widget.Material3.Chip.Assist
diff --git a/lib/java/com/google/android/material/theme/res/values/themes_base.xml b/lib/java/com/google/android/material/theme/res/values/themes_base.xml
index 0f16d1dc6a5..a69ea7a22b6 100644
--- a/lib/java/com/google/android/material/theme/res/values/themes_base.xml
+++ b/lib/java/com/google/android/material/theme/res/values/themes_base.xml
@@ -92,6 +92,7 @@
- @style/Widget.Material3.Button.TextButton
- @style/Widget.Material3.BottomAppBar
- @style/Widget.Material3.BottomNavigationView
+ - @style/Widget.Material3.BottomSheet.DragHandle
- @style/Widget.Material3.Button.TextButton.Dialog
- @style/Widget.Material3.CompoundButton.CheckBox
- @style/Widget.Material3.Chip.Assist
@@ -328,6 +329,7 @@
- @style/Widget.Material3.Button.TextButton
- @style/Widget.Material3.BottomAppBar
- @style/Widget.Material3.BottomNavigationView
+ - @style/Widget.Material3.BottomSheet.DragHandle
- @style/Widget.Material3.Button.TextButton.Dialog
- @style/Widget.Material3.CompoundButton.CheckBox
- @style/Widget.Material3.Chip.Assist
diff --git a/lib/javatests/com/google/android/material/bottomsheet/BottomSheetDragHandleTest.java b/lib/javatests/com/google/android/material/bottomsheet/BottomSheetDragHandleTest.java
new file mode 100644
index 00000000000..5561c73079f
--- /dev/null
+++ b/lib/javatests/com/google/android/material/bottomsheet/BottomSheetDragHandleTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.material.bottomsheet;
+
+import com.google.android.material.test.R;
+
+import static android.content.Context.ACCESSIBILITY_SERVICE;
+import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.FrameLayout;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.core.view.ViewCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class BottomSheetDragHandleTest {
+ private AccessibilityManager accessibilityManager;
+ private TestActivity activity;
+ private BottomSheetDragHandleView dragHandleView;
+
+ @Before
+ public void setUp() throws Exception {
+ accessibilityManager =
+ (AccessibilityManager)
+ ApplicationProvider.getApplicationContext().getSystemService(ACCESSIBILITY_SERVICE);
+
+ activity = Robolectric.buildActivity(TestActivity.class).setup().get();
+
+ dragHandleView = new BottomSheetDragHandleView(activity);
+ }
+
+ @Test
+ public void test_notInteractableWhenDetachedAndAccessibilityDisabled() {
+ assertImportantForAccessibility(false);
+ assertThat(dragHandleView.isClickable()).isFalse();
+ }
+
+ @Test
+ public void test_notInteractableWhenDetachedAndAccessibilityEnabled() {
+ shadowOf(accessibilityManager).setEnabled(true);
+ assertImportantForAccessibility(false);
+ assertThat(dragHandleView.isClickable()).isFalse();
+ }
+
+ @Test
+ public void test_notInteractableWhenAttachedAndAccessibilityDisabled() {
+ activity.addViewToBottomSheet(dragHandleView);
+ assertImportantForAccessibility(true);
+ assertThat(dragHandleView.isClickable()).isFalse();
+ }
+
+ @Test
+ public void test_notInteractableWhenNotAttachedToBottomSheetAndAccessibilityEnabled() {
+ activity.addViewToContainer(dragHandleView);
+ shadowOf(accessibilityManager).setEnabled(true);
+ assertImportantForAccessibility(false);
+ assertThat(dragHandleView.isClickable()).isFalse();
+ }
+
+ @Test
+ public void test_interactableWhenAttachedAndAccessibilityEnabled() {
+ activity.addViewToBottomSheet(dragHandleView);
+ shadowOf(accessibilityManager).setEnabled(true);
+ assertImportantForAccessibility(true);
+ assertThat(dragHandleView.isClickable()).isTrue();
+ }
+
+ @Test
+ public void test_expandCollapsedBottomSheetWhenClicked() {
+ activity.bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ activity.addViewToBottomSheet(dragHandleView);
+ shadowOf(accessibilityManager).setEnabled(true);
+ dragHandleView.performClick();
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ assertThat(activity.bottomSheetBehavior.getState())
+ .isEqualTo(BottomSheetBehavior.STATE_EXPANDED);
+ }
+
+ @Test
+ public void test_collapseExpandedBottomSheetWhenClicked() {
+ activity.bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ activity.addViewToBottomSheet(dragHandleView);
+ shadowOf(accessibilityManager).setEnabled(true);
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ dragHandleView.performClick();
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ assertThat(activity.bottomSheetBehavior.getState())
+ .isEqualTo(BottomSheetBehavior.STATE_COLLAPSED);
+ }
+
+ private void assertImportantForAccessibility(boolean important) {
+ if (important) {
+ assertThat(ViewCompat.getImportantForAccessibility(dragHandleView))
+ .isEqualTo(ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ } else {
+ assertThat(ViewCompat.getImportantForAccessibility(dragHandleView))
+ .isEqualTo(ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ }
+ }
+
+ private static class TestActivity extends AppCompatActivity {
+ @Nullable
+ private CoordinatorLayout container;
+
+ @Nullable
+ private FrameLayout bottomSheet;
+
+ @NonNull
+ private final BottomSheetBehavior bottomSheetBehavior = new BottomSheetBehavior<>();
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ setTheme(R.style.Theme_Material3_Light_NoActionBar);
+ container = new CoordinatorLayout(this);
+ setContentView(
+ container, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ bottomSheet = new FrameLayout(this);
+ CoordinatorLayout.LayoutParams layoutParams =
+ new CoordinatorLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ layoutParams.setBehavior(bottomSheetBehavior);
+ container.addView(bottomSheet, layoutParams);
+ }
+
+ void addViewToContainer(View view) {
+ if (container == null) {
+ return;
+ }
+ container.addView(view);
+ }
+
+ void addViewToBottomSheet(View view) {
+ if (bottomSheet == null) {
+ return;
+ }
+ bottomSheet.addView(view);
+ }
+ }
+}
diff --git a/lib/javatests/com/google/android/material/theme/ThemeTest.java b/lib/javatests/com/google/android/material/theme/ThemeTest.java
index 7582b7cfb2c..27f7ae163a3 100644
--- a/lib/javatests/com/google/android/material/theme/ThemeTest.java
+++ b/lib/javatests/com/google/android/material/theme/ThemeTest.java
@@ -343,6 +343,7 @@ public class ThemeTest {
R.attr.borderlessButtonStyle,
R.attr.bottomAppBarStyle,
R.attr.bottomNavigationStyle,
+ R.attr.bottomSheetDragHandleStyle,
R.attr.buttonBarButtonStyle,
R.attr.checkboxStyle,
R.attr.chipStyle,