Skip to content

Commit

Permalink
[BottomSheet] Create a drag handle view which support accessibility c…
Browse files Browse the repository at this point in the history
…ontrol

PiperOrigin-RevId: 457939228
  • Loading branch information
drchen authored and raajkumars committed Jun 29, 2022
1 parent a9752ee commit ac7b761
Show file tree
Hide file tree
Showing 16 changed files with 470 additions and 1 deletion.
Expand Up @@ -19,6 +19,11 @@
android:id="@+id/bottom_drawer_2"
android:layout_width="match_parent"
android:layout_height="600dp">

<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

<TextView
android:id="@+id/bottomsheet_state"
android:layout_width="match_parent"
Expand Down
Expand Up @@ -65,11 +65,16 @@
app:behavior_peekHeight="200dp"
app:layout_behavior="@string/bottom_sheet_behavior">

<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_below="@id/drag_handle"
android:layout_centerHorizontal="true"
android:text="@string/cat_bottomsheet_persistent"
android:textColor="?attr/colorOnSurface"
Expand Down
19 changes: 19 additions & 0 deletions docs/components/BottomSheet.md
Expand Up @@ -225,6 +225,19 @@ slides under the status bar, to prevent content from being drawn underneath it.
The contents within a bottom sheet should follow their own accessibility
guidelines, such as setting content descriptions for images.

To support dragging bottom sheets with accessibility services such as TalkBack,
Voice Access, Switch Access, etc., we provide a convenient widget
`BottomSheetDragHandleView` which will automatically receive and handle
accessibility commands to expand and collapse the attached bottom sheet when
the accessibility mode is enabled. To use `BottomSheetDragHandleView`, you can
add it to the top of your bottom sheet content. It will show a customizable
visual indicator for all users. See the example in the below section for how to
add a drag handle to your bottom sheet.

**Note:** `BottomSheetDragHandleView` has a default min width and height of 48dp
to conform to the minimum touch target requirement. So you will need to preserve
at least 48dp at the top to place a drag handle.

## Standard bottom sheet

Standard bottom sheets co-exist with the screen’s main UI region and allow for
Expand Down Expand Up @@ -269,6 +282,12 @@ Apply the `BottomSheetBehavior` to a direct child `View` of `CoordinatorLayout`:
style="?attr/bottomSheetStyle"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

<!-- Drag handle for accessibility -->
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

<!-- Bottom sheet contents. -->
<TextView
android:layout_width="wrap_content"
Expand Down
Binary file modified docs/components/assets/bottomsheet/bottomsheet_modal.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/components/assets/bottomsheet/bottomsheet_standard.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,218 @@
/*
* 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.R;

import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED;
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;

import android.content.Context;
import androidx.appcompat.widget.AppCompatImageView;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback;

/**
* A drag handle view that can be added to bottom sheets associated with {@link
* BottomSheetBehavior}. This view will automatically handle the accessibility interaction when the
* accessibility service is enabled. When you add a drag handle to a bottom sheet and the user
* enables the accessibility service, the drag handle will become important for accessibility and
* clickable. Clicking the drag handle will toggle the bottom sheet between its collapsed and
* expanded states.
*/
public class BottomSheetDragHandleView extends AppCompatImageView
implements AccessibilityStateChangeListener {
private static final int DEF_STYLE_RES = R.style.Widget_Material3_BottomSheet_DragHandle;

@Nullable private final AccessibilityManager accessibilityManager;

@Nullable private BottomSheetBehavior<?> 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;
}
}
Expand Up @@ -22,6 +22,7 @@
<public name="Theme.Design.BottomSheetDialog" type="style"/>
<public name="bottomSheetDialogTheme" type="attr"/>
<public name="bottomSheetStyle" type="attr"/>
<public name="bottomSheetDragHandleStyle" type="attr"/>
<public name="behavior_peekHeight" type="attr"/>
<public name="backgroundTint" type="attr"/>
<public name="behavior_fitToContents" type="attr"/>
Expand All @@ -41,6 +42,7 @@
<public name="ThemeOverlay.Material3.DayNight.BottomSheetDialog" type="style"/>
<public name="Widget.Material3.BottomSheet" type="style"/>
<public name="Widget.Material3.BottomSheet.Modal" type="style"/>
<public name="Widget.Material3.BottomSheet.DragHandle" type="style"/>
<public name="Theme.Material3.Light.BottomSheetDialog" type="style"/>
<public name="Theme.Material3.Dark.BottomSheetDialog" type="style"/>
<public name="Theme.Material3.DayNight.BottomSheetDialog" type="style"/>
Expand Down
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 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.
-->

<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:height="4dp" android:width="32dp" />
<corners android:radius="2dp" />
<solid android:color="#FFF" />
</shape>
Expand Up @@ -22,6 +22,8 @@
<attr name="bottomSheetStyle" format="reference"/>
<!-- Determines if the BottomSheetDialog should be shown in edge to edge mode. -->
<attr name="enableEdgeToEdge" format="boolean" />
<!-- Style of drag handle views when being used with bottom sheets. -->
<attr name="bottomSheetDragHandleStyle" format="reference"/>

<declare-styleable name="BottomSheetBehavior_Layout">
<!-- The height of the bottom sheet when it is collapsed. -->
Expand Down
Expand Up @@ -25,4 +25,5 @@
<dimen name="m3_bottom_sheet_elevation">@dimen/m3_sys_elevation_level2</dimen>
<dimen name="m3_bottom_sheet_modal_elevation">@dimen/m3_sys_elevation_level3</dimen>

<dimen name="m3_bottom_sheet_drag_handle_bottom_padding">20dp</dimen>
</resources>
Expand Up @@ -19,4 +19,9 @@
<string name="bottom_sheet_behavior" translatable="false">com.google.android.material.bottomsheet.BottomSheetBehavior</string>
<string description="A description of an action that a user of an accessibility service can perform to expand a bottomsheet halfway to its full height. [CHAR LIMIT=25]"
name="bottomsheet_action_expand_halfway">Expand halfway</string>

<string description="The content description of bottom sheet drag handles [CHAR LIMIT=NONE]" name="bottomsheet_drag_handle_content_description">Drag handle</string>
<string description="A description of an accessibility action to expand the bottom sheet [CHAR LIMIT=NONE]" name="bottomsheet_action_expand">Expand the bottom sheet</string>
<string description="A description of an accessibility action to collapse the bottom sheet [CHAR LIMIT=NONE]" name="bottomsheet_action_collapse">Collapse the bottom sheet</string>
<string description="An accessibility feedback when the bottom sheet drag handle is clicked [CHAR LIMIT=NONE]" name="bottomsheet_drag_handle_clicked">Drag handle double-tapped</string>
</resources>
Expand Up @@ -80,4 +80,15 @@
<style name="Widget.Material3.BottomSheet.Modal">
<item name="android:elevation" tools:ignore="NewApi">@dimen/m3_bottom_sheet_modal_elevation</item>
</style>

<!-- The default M3 style of BottomSheetDragHandleView -->
<style name="Widget.Material3.BottomSheet.DragHandle" parent="">
<item name="android:scaleType">center</item>
<item name="android:contentDescription">@string/bottomsheet_drag_handle_content_description</item>
<item name="android:minWidth">@dimen/mtrl_min_touch_target_size</item>
<item name="android:minHeight">@dimen/mtrl_min_touch_target_size</item>
<item name="android:paddingBottom">@dimen/m3_bottom_sheet_drag_handle_bottom_padding</item>
<item name="srcCompat">@drawable/mtrl_bottomsheet_drag_handle</item>
<item name="tint">?attr/colorOnSurfaceVariant</item>
</style>
</resources>
Expand Up @@ -78,6 +78,7 @@
<item name="borderlessButtonStyle">@style/Widget.Material3.Button.TextButton</item>
<item name="bottomAppBarStyle">@style/Widget.Material3.BottomAppBar</item>
<item name="bottomNavigationStyle">@style/Widget.Material3.BottomNavigationView</item>
<item name="bottomSheetDragHandleStyle">@style/Widget.Material3.BottomSheet.DragHandle</item>
<item name="buttonBarButtonStyle">@style/Widget.Material3.Button.TextButton.Dialog</item>
<item name="checkboxStyle">@style/Widget.Material3.CompoundButton.CheckBox</item>
<item name="chipStyle">@style/Widget.Material3.Chip.Assist</item>
Expand Down Expand Up @@ -312,6 +313,7 @@
<item name="borderlessButtonStyle">@style/Widget.Material3.Button.TextButton</item>
<item name="bottomAppBarStyle">@style/Widget.Material3.BottomAppBar</item>
<item name="bottomNavigationStyle">@style/Widget.Material3.BottomNavigationView</item>
<item name="bottomSheetDragHandleStyle">@style/Widget.Material3.BottomSheet.DragHandle</item>
<item name="buttonBarButtonStyle">@style/Widget.Material3.Button.TextButton.Dialog</item>
<item name="checkboxStyle">@style/Widget.Material3.CompoundButton.CheckBox</item>
<item name="chipStyle">@style/Widget.Material3.Chip.Assist</item>
Expand Down
Expand Up @@ -92,6 +92,7 @@
<item name="borderlessButtonStyle">@style/Widget.Material3.Button.TextButton</item>
<item name="bottomAppBarStyle">@style/Widget.Material3.BottomAppBar</item>
<item name="bottomNavigationStyle">@style/Widget.Material3.BottomNavigationView</item>
<item name="bottomSheetDragHandleStyle">@style/Widget.Material3.BottomSheet.DragHandle</item>
<item name="buttonBarButtonStyle">@style/Widget.Material3.Button.TextButton.Dialog</item>
<item name="checkboxStyle">@style/Widget.Material3.CompoundButton.CheckBox</item>
<item name="chipStyle">@style/Widget.Material3.Chip.Assist</item>
Expand Down Expand Up @@ -328,6 +329,7 @@
<item name="borderlessButtonStyle">@style/Widget.Material3.Button.TextButton</item>
<item name="bottomAppBarStyle">@style/Widget.Material3.BottomAppBar</item>
<item name="bottomNavigationStyle">@style/Widget.Material3.BottomNavigationView</item>
<item name="bottomSheetDragHandleStyle">@style/Widget.Material3.BottomSheet.DragHandle</item>
<item name="buttonBarButtonStyle">@style/Widget.Material3.Button.TextButton.Dialog</item>
<item name="checkboxStyle">@style/Widget.Material3.CompoundButton.CheckBox</item>
<item name="chipStyle">@style/Widget.Material3.Chip.Assist</item>
Expand Down

0 comments on commit ac7b761

Please sign in to comment.