2121import static android .view .accessibility .AccessibilityEvent .TYPE_VIEW_CLICKED ;
2222import static com .google .android .material .theme .overlay .MaterialThemeOverlay .wrap ;
2323
24+ import android .annotation .SuppressLint ;
2425import android .content .Context ;
26+ import android .os .Handler ;
27+ import android .os .Looper ;
2528import androidx .appcompat .widget .AppCompatImageView ;
2629import android .util .AttributeSet ;
30+ import android .view .GestureDetector ;
31+ import android .view .GestureDetector .OnGestureListener ;
32+ import android .view .GestureDetector .SimpleOnGestureListener ;
33+ import android .view .MotionEvent ;
2734import android .view .View ;
2835import android .view .ViewGroup .LayoutParams ;
2936import android .view .ViewParent ;
3037import android .view .accessibility .AccessibilityEvent ;
31- import android .view .accessibility .AccessibilityManager ;
32- import android .view .accessibility .AccessibilityManager .AccessibilityStateChangeListener ;
3338import androidx .annotation .NonNull ;
3439import androidx .annotation .Nullable ;
3540import androidx .coordinatorlayout .widget .CoordinatorLayout ;
4651 * clickable. Clicking the drag handle will toggle the bottom sheet between its collapsed and
4752 * expanded states.
4853 */
49- public class BottomSheetDragHandleView extends AppCompatImageView
50- implements AccessibilityStateChangeListener {
54+ public class BottomSheetDragHandleView extends AppCompatImageView {
5155 private static final int DEF_STYLE_RES = R .style .Widget_Material3_BottomSheet_DragHandle ;
5256
53- @ Nullable private final AccessibilityManager accessibilityManager ;
54-
5557 @ Nullable private BottomSheetBehavior <?> bottomSheetBehavior ;
5658
57- private boolean accessibilityServiceEnabled ;
58- private boolean interactable ;
59+ private final GestureDetector gestureDetector ;
60+
5961 private boolean clickToExpand ;
6062
63+ /**
64+ * Track whether clients have set their own touch or click listeners on the drag handle.
65+ *
66+ * Setting a custom touch or click listener will override the default behavior of cycling through
67+ * bottom sheet states when tapped and dismissing the sheet when double tapped. Clients can
68+ * restore this behavior by setting their touch and click listeners back to null.
69+ */
70+ private boolean hasTouchListener = false ;
71+ private boolean hasClickListener = false ;
72+
6173 private final String clickToExpandActionLabel =
6274 getResources ().getString (R .string .bottomsheet_action_expand );
6375 private final String clickToCollapseActionLabel =
@@ -75,6 +87,34 @@ public void onStateChanged(
7587 public void onSlide (@ NonNull View bottomSheet , float slideOffset ) {}
7688 };
7789
90+ /**
91+ * A gesture listener that handles both single and double taps on the drag handle.
92+ *
93+ * Single taps cycle through the available states of the bottom sheet. A double tap hides
94+ * the sheet.
95+ */
96+ private final OnGestureListener gestureListener = new SimpleOnGestureListener () {
97+
98+ @ Override
99+ public boolean onDown (@ NonNull MotionEvent e ) {
100+ return isClickable ();
101+ }
102+
103+ @ Override
104+ public boolean onSingleTapConfirmed (@ NonNull MotionEvent e ) {
105+ return expandOrCollapseBottomSheetIfPossible ();
106+ }
107+
108+ @ Override
109+ public boolean onDoubleTap (@ NonNull MotionEvent e ) {
110+ if (bottomSheetBehavior != null && bottomSheetBehavior .isHideable ()) {
111+ bottomSheetBehavior .setState (BottomSheetBehavior .STATE_HIDDEN );
112+ return true ;
113+ }
114+ return super .onDoubleTap (e );
115+ }
116+ };
117+
78118 public BottomSheetDragHandleView (@ NonNull Context context ) {
79119 this (context , /* attrs= */ null );
80120 }
@@ -83,17 +123,16 @@ public BottomSheetDragHandleView(@NonNull Context context, @Nullable AttributeSe
83123 this (context , attrs , R .attr .bottomSheetDragHandleStyle );
84124 }
85125
126+ @ SuppressLint ("ClickableViewAccessibility" ) // Will be handled by accessibility delegate
86127 public BottomSheetDragHandleView (
87128 @ NonNull Context context , @ Nullable AttributeSet attrs , int defStyleAttr ) {
88129 super (wrap (context , attrs , defStyleAttr , DEF_STYLE_RES ), attrs , defStyleAttr );
89130
90131 // Override the provided context with the wrapped one to prevent it from being used.
91132 context = getContext ();
92133
93- accessibilityManager =
94- (AccessibilityManager ) context .getSystemService (Context .ACCESSIBILITY_SERVICE );
95-
96- updateInteractableState ();
134+ gestureDetector =
135+ new GestureDetector (context , gestureListener , new Handler (Looper .getMainLooper ()));
97136
98137 ViewCompat .setAccessibilityDelegate (
99138 this ,
@@ -112,25 +151,36 @@ public void onPopulateAccessibilityEvent(View host, @NonNull AccessibilityEvent
112151 protected void onAttachedToWindow () {
113152 super .onAttachedToWindow ();
114153 setBottomSheetBehavior (findParentBottomSheetBehavior ());
115- if (accessibilityManager != null ) {
116- accessibilityManager .addAccessibilityStateChangeListener (this );
117- onAccessibilityStateChanged (accessibilityManager .isEnabled ());
118- }
119154 }
120155
121156 @ Override
122157 protected void onDetachedFromWindow () {
123- if (accessibilityManager != null ) {
124- accessibilityManager .removeAccessibilityStateChangeListener (this );
125- }
126158 setBottomSheetBehavior (null );
127159 super .onDetachedFromWindow ();
128160 }
129161
162+ @ SuppressLint ("ClickableViewAccessibility" ) // Will be handled by accessibility delegate
163+ @ Override
164+ public boolean onTouchEvent (MotionEvent event ) {
165+ if (hasClickListener || hasTouchListener ) {
166+ // If clients have set their own click or touch listeners, do nothing.
167+ return super .onTouchEvent (event );
168+ }
169+
170+ return gestureDetector .onTouchEvent (event );
171+ }
172+
173+ @ SuppressLint ("ClickableViewAccessibility" )
174+ @ Override
175+ public void setOnTouchListener (OnTouchListener l ) {
176+ hasTouchListener = l != null ;
177+ super .setOnTouchListener (l );
178+ }
179+
130180 @ Override
131- public void onAccessibilityStateChanged ( boolean enabled ) {
132- accessibilityServiceEnabled = enabled ;
133- updateInteractableState ( );
181+ public void setOnClickListener ( @ Nullable OnClickListener l ) {
182+ hasClickListener = l != null ;
183+ super . setOnClickListener ( l );
134184 }
135185
136186 private void setBottomSheetBehavior (@ Nullable BottomSheetBehavior <?> behavior ) {
@@ -144,7 +194,7 @@ private void setBottomSheetBehavior(@Nullable BottomSheetBehavior<?> behavior) {
144194 onBottomSheetStateChanged (bottomSheetBehavior .getState ());
145195 bottomSheetBehavior .addBottomSheetCallback (bottomSheetCallback );
146196 }
147- updateInteractableState ( );
197+ setClickable ( hasAttachedBehavior () );
148198 }
149199
150200 private void onBottomSheetStateChanged (@ BottomSheetBehavior .State int state ) {
@@ -160,12 +210,8 @@ private void onBottomSheetStateChanged(@BottomSheetBehavior.State int state) {
160210 (v , args ) -> expandOrCollapseBottomSheetIfPossible ());
161211 }
162212
163- private void updateInteractableState () {
164- interactable = accessibilityServiceEnabled && bottomSheetBehavior != null ;
165- setImportantForAccessibility (bottomSheetBehavior != null
166- ? View .IMPORTANT_FOR_ACCESSIBILITY_YES
167- : View .IMPORTANT_FOR_ACCESSIBILITY_NO );
168- setClickable (interactable );
213+ private boolean hasAttachedBehavior () {
214+ return bottomSheetBehavior != null ;
169215 }
170216
171217 /**
@@ -179,7 +225,7 @@ private void updateInteractableState() {
179225 * the previous state was EXPANDED) or EXPANDED (when the previous state was COLLAPSED.)
180226 */
181227 private boolean expandOrCollapseBottomSheetIfPossible () {
182- if (!interactable ) {
228+ if (!hasAttachedBehavior () ) {
183229 return false ;
184230 }
185231 boolean canHalfExpand =
0 commit comments