Skip to content

Commit 0804031

Browse files
drchenpaulfthomas
authored andcommitted
[BottomSheet] Sync custom actions with drag handle views
Custom actions need to be set directly on the focused child views to make talkback announce the existence of those actions correctly, despite that when you open custom action menu you can actually see they are being inherited from the parent view. Makes BottomSheetBehavior be aware of the existence of accessibility delegate views, and update the custom actions on it when needed. PiperOrigin-RevId: 478804858
1 parent e67e68d commit 0804031

File tree

3 files changed

+217
-21
lines changed

3 files changed

+217
-21
lines changed

lib/java/com/google/android/material/bottomsheet/BottomSheetBehavior.java

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import android.os.Parcelable;
3535
import android.util.AttributeSet;
3636
import android.util.Log;
37+
import android.util.SparseIntArray;
3738
import android.util.TypedValue;
3839
import android.view.MotionEvent;
3940
import android.view.VelocityTracker;
@@ -212,6 +213,11 @@ void onLayout(@NonNull View bottomSheet) {}
212213

213214
private static final int NO_MAX_SIZE = -1;
214215

216+
private static final int VIEW_INDEX_BOTTOM_SHEET = 0;
217+
218+
@VisibleForTesting
219+
static final int VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW = 1;
220+
215221
private boolean fitToContents = true;
216222

217223
private boolean updateImportantForAccessibilityOnSiblings = false;
@@ -303,6 +309,7 @@ void onLayout(@NonNull View bottomSheet) {}
303309
int parentHeight;
304310

305311
@Nullable WeakReference<V> viewRef;
312+
@Nullable WeakReference<View> accessibilityDelegateViewRef;
306313

307314
@Nullable WeakReference<View> nestedScrollingChildRef;
308315

@@ -318,7 +325,8 @@ void onLayout(@NonNull View bottomSheet) {}
318325

319326
@Nullable private Map<View, Integer> importantForAccessibilityMap;
320327

321-
private int expandHalfwayActionId = View.NO_ID;
328+
@VisibleForTesting
329+
final SparseIntArray expandHalfwayActionIds = new SparseIntArray();
322330

323331
public BottomSheetBehavior() {}
324332

@@ -2156,67 +2164,98 @@ private void updateImportantForAccessibility(boolean expanded) {
21562164
}
21572165
}
21582166

2159-
private void updateAccessibilityActions() {
2160-
if (viewRef == null) {
2167+
void setAccessibilityDelegateView(@Nullable View accessibilityDelegateView) {
2168+
if (accessibilityDelegateView == null && accessibilityDelegateViewRef != null) {
2169+
clearAccessibilityAction(
2170+
accessibilityDelegateViewRef.get(), VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW);
2171+
accessibilityDelegateViewRef = null;
21612172
return;
21622173
}
2163-
V child = viewRef.get();
2164-
if (child == null) {
2165-
return;
2174+
accessibilityDelegateViewRef = new WeakReference<>(accessibilityDelegateView);
2175+
updateAccessibilityActions(accessibilityDelegateView, VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW);
2176+
}
2177+
2178+
private void updateAccessibilityActions() {
2179+
if (viewRef != null) {
2180+
updateAccessibilityActions(viewRef.get(), VIEW_INDEX_BOTTOM_SHEET);
2181+
}
2182+
if (accessibilityDelegateViewRef != null) {
2183+
updateAccessibilityActions(
2184+
accessibilityDelegateViewRef.get(), VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW);
21662185
}
2167-
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_COLLAPSE);
2168-
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_EXPAND);
2169-
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_DISMISS);
2186+
}
21702187

2171-
if (expandHalfwayActionId != View.NO_ID) {
2172-
ViewCompat.removeAccessibilityAction(child, expandHalfwayActionId);
2188+
private void updateAccessibilityActions(View view, int viewIndex) {
2189+
if (view == null) {
2190+
return;
21732191
}
2192+
clearAccessibilityAction(view, viewIndex);
2193+
21742194
if (!fitToContents && state != STATE_HALF_EXPANDED) {
2175-
expandHalfwayActionId =
2195+
expandHalfwayActionIds.put(
2196+
viewIndex,
21762197
addAccessibilityActionForState(
2177-
child, R.string.bottomsheet_action_expand_halfway, STATE_HALF_EXPANDED);
2198+
view, R.string.bottomsheet_action_expand_halfway, STATE_HALF_EXPANDED));
21782199
}
21792200

21802201
if ((hideable && isHideableWhenDragging()) && state != STATE_HIDDEN) {
21812202
replaceAccessibilityActionForState(
2182-
child, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN);
2203+
view, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN);
21832204
}
21842205

21852206
switch (state) {
21862207
case STATE_EXPANDED:
21872208
{
21882209
int nextState = fitToContents ? STATE_COLLAPSED : STATE_HALF_EXPANDED;
21892210
replaceAccessibilityActionForState(
2190-
child, AccessibilityActionCompat.ACTION_COLLAPSE, nextState);
2211+
view, AccessibilityActionCompat.ACTION_COLLAPSE, nextState);
21912212
break;
21922213
}
21932214
case STATE_HALF_EXPANDED:
21942215
{
21952216
replaceAccessibilityActionForState(
2196-
child, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED);
2217+
view, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED);
21972218
replaceAccessibilityActionForState(
2198-
child, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED);
2219+
view, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED);
21992220
break;
22002221
}
22012222
case STATE_COLLAPSED:
22022223
{
22032224
int nextState = fitToContents ? STATE_EXPANDED : STATE_HALF_EXPANDED;
22042225
replaceAccessibilityActionForState(
2205-
child, AccessibilityActionCompat.ACTION_EXPAND, nextState);
2226+
view, AccessibilityActionCompat.ACTION_EXPAND, nextState);
22062227
break;
22072228
}
2208-
default: // fall out
2229+
case STATE_HIDDEN:
2230+
case STATE_DRAGGING:
2231+
case STATE_SETTLING:
2232+
// Accessibility actions are not applicable, do nothing
2233+
}
2234+
}
2235+
2236+
private void clearAccessibilityAction(View view, int viewIndex) {
2237+
if (view == null) {
2238+
return;
2239+
}
2240+
ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_COLLAPSE);
2241+
ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_EXPAND);
2242+
ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_DISMISS);
2243+
2244+
int expandHalfwayActionId = expandHalfwayActionIds.get(viewIndex, View.NO_ID);
2245+
if (expandHalfwayActionId != View.NO_ID) {
2246+
ViewCompat.removeAccessibilityAction(view, expandHalfwayActionId);
2247+
expandHalfwayActionIds.delete(viewIndex);
22092248
}
22102249
}
22112250

22122251
private void replaceAccessibilityActionForState(
2213-
V child, AccessibilityActionCompat action, @State int state) {
2252+
View child, AccessibilityActionCompat action, @State int state) {
22142253
ViewCompat.replaceAccessibilityAction(
22152254
child, action, null, createAccessibilityViewCommandForState(state));
22162255
}
22172256

22182257
private int addAccessibilityActionForState(
2219-
V child, @StringRes int stringResId, @State int state) {
2258+
View child, @StringRes int stringResId, @State int state) {
22202259
return ViewCompat.addAccessibilityAction(
22212260
child,
22222261
child.getResources().getString(stringResId),

lib/java/com/google/android/material/bottomsheet/BottomSheetDragHandleView.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,11 @@ public void onAccessibilityStateChanged(boolean enabled) {
139139
private void setBottomSheetBehavior(@Nullable BottomSheetBehavior<?> behavior) {
140140
if (bottomSheetBehavior != null) {
141141
bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback);
142+
bottomSheetBehavior.setAccessibilityDelegateView(null);
142143
}
143144
bottomSheetBehavior = behavior;
144145
if (bottomSheetBehavior != null) {
146+
bottomSheetBehavior.setAccessibilityDelegateView(this);
145147
onBottomSheetStateChanged(bottomSheetBehavior.getState());
146148
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
147149
}

lib/javatests/com/google/android/material/bottomsheet/BottomSheetDragHandleTest.java

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
import com.google.android.material.test.R;
2020

2121
import static android.content.Context.ACCESSIBILITY_SERVICE;
22+
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_COLLAPSE;
23+
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_DISMISS;
24+
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_EXPAND;
25+
import static com.google.android.material.bottomsheet.BottomSheetBehavior.VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW;
2226
import static com.google.common.truth.Truth.assertThat;
2327
import static org.robolectric.Shadows.shadowOf;
2428

@@ -32,8 +36,10 @@
3236
import androidx.annotation.Nullable;
3337
import androidx.coordinatorlayout.widget.CoordinatorLayout;
3438
import androidx.core.view.ViewCompat;
39+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
3540
import androidx.test.core.app.ApplicationProvider;
3641
import androidx.test.platform.app.InstrumentationRegistry;
42+
import java.util.ArrayList;
3743
import org.junit.Before;
3844
import org.junit.Test;
3945
import org.junit.runner.RunWith;
@@ -195,6 +201,140 @@ public void test_halfExpandedBottomSheetMoveToCollapsed_whenPreviouslyExpanded()
195201
.isEqualTo(BottomSheetBehavior.STATE_COLLAPSED);
196202
}
197203

204+
@Test
205+
public void test_customActionExpand() {
206+
activity.bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
207+
activity.addViewToBottomSheet(dragHandleView);
208+
shadowOf(accessibilityManager).setEnabled(true);
209+
210+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
211+
212+
ViewCompat.performAccessibilityAction(dragHandleView, ACTION_EXPAND.getId(), /* args= */ null);
213+
214+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
215+
216+
assertThat(activity.bottomSheetBehavior.getState())
217+
.isEqualTo(BottomSheetBehavior.STATE_EXPANDED);
218+
}
219+
220+
@Test
221+
public void test_customActionHalfExpand() {
222+
activity.bottomSheetBehavior.setFitToContents(false);
223+
activity.bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
224+
activity.addViewToBottomSheet(dragHandleView);
225+
shadowOf(accessibilityManager).setEnabled(true);
226+
227+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
228+
229+
ViewCompat.performAccessibilityAction(
230+
dragHandleView, getHalfExpandActionId(), /* args= */ null);
231+
232+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
233+
234+
assertThat(activity.bottomSheetBehavior.getState())
235+
.isEqualTo(BottomSheetBehavior.STATE_HALF_EXPANDED);
236+
}
237+
238+
@Test
239+
public void test_customActionCollapse() {
240+
activity.bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
241+
activity.addViewToBottomSheet(dragHandleView);
242+
shadowOf(accessibilityManager).setEnabled(true);
243+
244+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
245+
246+
ViewCompat.performAccessibilityAction(
247+
dragHandleView, ACTION_COLLAPSE.getId(), /* args= */ null);
248+
249+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
250+
251+
assertThat(activity.bottomSheetBehavior.getState())
252+
.isEqualTo(BottomSheetBehavior.STATE_COLLAPSED);
253+
}
254+
255+
@Test
256+
public void test_customActionDismiss() {
257+
activity.bottomSheetBehavior.setHideable(true);
258+
activity.bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
259+
activity.addViewToBottomSheet(dragHandleView);
260+
shadowOf(accessibilityManager).setEnabled(true);
261+
262+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
263+
264+
ViewCompat.performAccessibilityAction(dragHandleView, ACTION_DISMISS.getId(), /* args= */ null);
265+
266+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
267+
268+
assertThat(activity.bottomSheetBehavior.getState()).isEqualTo(BottomSheetBehavior.STATE_HIDDEN);
269+
}
270+
271+
@Test
272+
public void test_customActionSetInCollapsedStateWhenHalfExpandableAndHideable() {
273+
activity.bottomSheetBehavior.setFitToContents(false);
274+
activity.bottomSheetBehavior.setHideable(true);
275+
activity.bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
276+
activity.addViewToBottomSheet(dragHandleView);
277+
shadowOf(accessibilityManager).setEnabled(true);
278+
279+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
280+
281+
assertThat(hasAccessibilityAction(dragHandleView, getHalfExpandActionId())).isTrue();
282+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_EXPAND.getId())).isTrue();
283+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_DISMISS.getId())).isTrue();
284+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_COLLAPSE.getId())).isFalse();
285+
}
286+
287+
@Test
288+
public void test_customActionSetInExpandedStateWhenHalfExpandableAndNotHideable() {
289+
activity.bottomSheetBehavior.setHideable(false);
290+
activity.bottomSheetBehavior.setFitToContents(false);
291+
activity.bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
292+
activity.addViewToBottomSheet(dragHandleView);
293+
shadowOf(accessibilityManager).setEnabled(true);
294+
295+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
296+
297+
assertThat(hasAccessibilityAction(dragHandleView, getHalfExpandActionId())).isTrue();
298+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_EXPAND.getId())).isFalse();
299+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_DISMISS.getId())).isFalse();
300+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_COLLAPSE.getId())).isTrue();
301+
}
302+
303+
@Test
304+
public void test_customActionSetInCollapsedStateWhenNotHideable() {
305+
activity.bottomSheetBehavior.setHideable(false);
306+
activity.bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
307+
activity.addViewToBottomSheet(dragHandleView);
308+
shadowOf(accessibilityManager).setEnabled(true);
309+
310+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
311+
312+
assertThat(getHalfExpandActionId()).isEqualTo(View.NO_ID);
313+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_EXPAND.getId())).isTrue();
314+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_DISMISS.getId())).isFalse();
315+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_COLLAPSE.getId())).isFalse();
316+
}
317+
318+
@Test
319+
public void test_customActionSetInExpandedStateWhenHideable() {
320+
activity.bottomSheetBehavior.setHideable(true);
321+
activity.bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
322+
activity.addViewToBottomSheet(dragHandleView);
323+
shadowOf(accessibilityManager).setEnabled(true);
324+
325+
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
326+
327+
assertThat(getHalfExpandActionId()).isEqualTo(View.NO_ID);
328+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_EXPAND.getId())).isFalse();
329+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_DISMISS.getId())).isTrue();
330+
assertThat(hasAccessibilityAction(dragHandleView, ACTION_COLLAPSE.getId())).isTrue();
331+
}
332+
333+
private int getHalfExpandActionId() {
334+
return activity.bottomSheetBehavior.expandHalfwayActionIds.get(
335+
VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW, View.NO_ID);
336+
}
337+
198338
private void assertImportantForAccessibility(boolean important) {
199339
if (important) {
200340
assertThat(ViewCompat.getImportantForAccessibility(dragHandleView))
@@ -205,6 +345,21 @@ private void assertImportantForAccessibility(boolean important) {
205345
}
206346
}
207347

348+
// TODO(b/250622249): remove duplicated methods after sharing test util classes
349+
private static boolean hasAccessibilityAction(View view, int actionId) {
350+
return getAccessibilityActionList(view).stream().anyMatch(action -> action.getId() == actionId);
351+
}
352+
353+
private static ArrayList<AccessibilityActionCompat> getAccessibilityActionList(View view) {
354+
@SuppressWarnings({"unchecked"})
355+
ArrayList<AccessibilityActionCompat> actions =
356+
(ArrayList<AccessibilityActionCompat>) view.getTag(R.id.tag_accessibility_actions);
357+
if (actions == null) {
358+
actions = new ArrayList<>();
359+
}
360+
return actions;
361+
}
362+
208363
private static class TestActivity extends AppCompatActivity {
209364
@Nullable
210365
private CoordinatorLayout container;

0 commit comments

Comments
 (0)