From 516240d4e5519838305e4129235f964a5f82ef16 Mon Sep 17 00:00:00 2001 From: afohrman Date: Wed, 4 Jan 2023 18:59:05 -0500 Subject: [PATCH] [Adaptive][Side Sheet] Added accessibilityPaneTitle to side sheet. This adds an accessibilityPaneTitle that is spoken by TalkBack on API levels 19 and later. In order to trigger the accessibilityPaneTitle event, it was necessary to add a visibility change when the sheet is expanded and hidden. The sheet now is INVISIBLE at STATE_HIDDEN and VISIBLE at all other states. Also removed the code to switch focus to the sheet on expansion in favor of this approach to align with TalkBack's APIs. PiperOrigin-RevId: 499604691 (cherry picked from commit 3b613275135e109cfd5f25379026b0f2c829ccdd) --- .../material/sidesheet/SideSheetBehavior.java | 50 ++++++---- .../material/sidesheet/res/values/strings.xml | 3 + .../sidesheet/SideSheetBehaviorTest.java | 97 +++++++++++++++++-- 3 files changed, 123 insertions(+), 27 deletions(-) diff --git a/lib/java/com/google/android/material/sidesheet/SideSheetBehavior.java b/lib/java/com/google/android/material/sidesheet/SideSheetBehavior.java index acf274dae61..fb55bd035f9 100644 --- a/lib/java/com/google/android/material/sidesheet/SideSheetBehavior.java +++ b/lib/java/com/google/android/material/sidesheet/SideSheetBehavior.java @@ -36,7 +36,6 @@ import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.view.ViewParent; -import android.view.accessibility.AccessibilityEvent; import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -64,6 +63,9 @@ public class SideSheetBehavior extends CoordinatorLayout.Behavior implements Sheet { + private static final int DEFAULT_ACCESSIBILITY_PANE_TITLE = + R.string.side_sheet_accessibility_pane_title; + private SheetDelegate sheetDelegate; static final int SIGNIFICANT_VEL_THRESHOLD = 500; @@ -288,11 +290,14 @@ public boolean onLayoutChild( } else if (backgroundTint != null) { ViewCompat.setBackgroundTintList(child, backgroundTint); } + updateSheetVisibility(child); + updateAccessibilityActions(); if (ViewCompat.getImportantForAccessibility(child) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); } + ensureAccessibilityPaneTitleIsSet(child); } if (viewDragHelper == null) { viewDragHelper = ViewDragHelper.create(parent, dragCallback); @@ -320,6 +325,23 @@ public boolean onLayoutChild( return true; } + private void updateSheetVisibility(@NonNull View sheet) { + // Sheet visibility is updated on state change to make TalkBack speak the accessibility pane + // title when the sheet expands. + int visibility = state == STATE_HIDDEN ? View.INVISIBLE : View.VISIBLE; + if (sheet.getVisibility() != visibility) { + sheet.setVisibility(visibility); + } + } + + private void ensureAccessibilityPaneTitleIsSet(View sheet) { + // Set default accessibility pane title that TalkBack will speak when the sheet is expanded. + if (ViewCompat.getAccessibilityPaneTitle(sheet) == null) { + ViewCompat.setAccessibilityPaneTitle( + sheet, sheet.getResources().getString(DEFAULT_ACCESSIBILITY_PANE_TITLE)); + } + } + private void maybeAssignCoplanarSiblingViewBasedId(@NonNull CoordinatorLayout parent) { if (coplanarSiblingViewRef == null && coplanarSiblingViewId != View.NO_ID) { View coplanarSiblingView = parent.findViewById(coplanarSiblingViewId); @@ -360,7 +382,7 @@ private int calculateCurrentOffset(int savedOutwardEdge, V child) { @Override public boolean onInterceptTouchEvent( @NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent event) { - if (!child.isShown() || !draggable) { + if (!shouldInterceptTouchEvent(child)) { ignoreEvents = true; return false; } @@ -392,6 +414,10 @@ public boolean onInterceptTouchEvent( && viewDragHelper.shouldInterceptTouchEvent(event); } + private boolean shouldInterceptTouchEvent(@NonNull V child) { + return (child.isShown() || ViewCompat.getAccessibilityPaneTitle(child) != null) && draggable; + } + int getSignificantVelocityThreshold() { return SIGNIFICANT_VEL_THRESHOLD; } @@ -578,9 +604,7 @@ void setStateInternal(@SheetState int state) { return; } - if (state == STATE_EXPANDED) { - updateAccessibilityFocusOnExpansion(); - } + updateSheetVisibility(sheet); for (SheetCallback callback : callbacks) { callback.onStateChanged(sheet, state); @@ -899,22 +923,6 @@ public static SideSheetBehavior from(@NonNull V view) { return (SideSheetBehavior) behavior; } - private void updateAccessibilityFocusOnExpansion() { - if (viewRef == null) { - return; - } - View view = viewRef.get(); - if (view instanceof ViewGroup && ((ViewGroup) view).getChildCount() > 0) { - ViewGroup viewContainer = (ViewGroup) view; - View firstNestedChild = viewContainer.getChildAt(0); - if (firstNestedChild != null) { - firstNestedChild.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - } - } else { - view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - } - } - private void updateAccessibilityActions() { if (viewRef == null) { return; diff --git a/lib/java/com/google/android/material/sidesheet/res/values/strings.xml b/lib/java/com/google/android/material/sidesheet/res/values/strings.xml index 506996e7a6b..e571ffef3a1 100644 --- a/lib/java/com/google/android/material/sidesheet/res/values/strings.xml +++ b/lib/java/com/google/android/material/sidesheet/res/values/strings.xml @@ -15,6 +15,9 @@ ~ limitations under the License. --> + + Side Sheet com.google.android.material.sidesheet.SideSheetBehavior diff --git a/lib/javatests/com/google/android/material/sidesheet/SideSheetBehaviorTest.java b/lib/javatests/com/google/android/material/sidesheet/SideSheetBehaviorTest.java index 01c27392ae4..95f10d4eab0 100644 --- a/lib/javatests/com/google/android/material/sidesheet/SideSheetBehaviorTest.java +++ b/lib/javatests/com/google/android/material/sidesheet/SideSheetBehaviorTest.java @@ -17,16 +17,23 @@ import com.google.android.material.test.R; +import static com.google.android.material.sidesheet.Sheet.STATE_DRAGGING; import static com.google.android.material.sidesheet.Sheet.STATE_EXPANDED; import static com.google.android.material.sidesheet.Sheet.STATE_HIDDEN; +import static com.google.android.material.sidesheet.Sheet.STATE_SETTLING; import static com.google.common.truth.Truth.assertThat; import static org.robolectric.Shadows.shadowOf; +import android.annotation.TargetApi; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Looper; import androidx.appcompat.app.AppCompatActivity; import android.view.View; import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.ViewCompat; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,22 +46,29 @@ public class SideSheetBehaviorTest { @NonNull TestActivity activity; + private View sideSheet; + private SideSheetBehavior sideSheetBehavior; + @Before public void createActivity() { activity = Robolectric.buildActivity(TestActivity.class).setup().get(); + CoordinatorLayout coordinatorLayout = + (CoordinatorLayout) activity.getLayoutInflater().inflate(R.layout.test_side_sheet, null); + sideSheet = coordinatorLayout.findViewById(R.id.test_side_sheet_container); + sideSheetBehavior = SideSheetBehavior.from(sideSheet); + activity.setContentView(coordinatorLayout); + + // Wait until the layout is measured. + shadowOf(Looper.getMainLooper()).idle(); } @Test public void onInitialization_sheetIsHidden() { - SideSheetBehavior sideSheetBehavior = new SideSheetBehavior<>(); - assertThat(sideSheetBehavior.getState()).isEqualTo(STATE_HIDDEN); } @Test public void expand_ofInitializedSheet_yieldsExpandedState() { - SideSheetBehavior sideSheetBehavior = new SideSheetBehavior<>(); - expandSheet(sideSheetBehavior); assertThat(sideSheetBehavior.getState()).isEqualTo(STATE_EXPANDED); @@ -62,7 +76,6 @@ public void expand_ofInitializedSheet_yieldsExpandedState() { @Test public void expand_ofExpandedSheet_isIdempotent() { - SideSheetBehavior sideSheetBehavior = new SideSheetBehavior<>(); expandSheet(sideSheetBehavior); assertThat(sideSheetBehavior.getState()).isEqualTo(STATE_EXPANDED); @@ -84,7 +97,6 @@ public void hide_ofExpandedSheet_yieldsHiddenState() { @Test public void hide_ofHiddenSheet_isIdempotent() { - SideSheetBehavior sideSheetBehavior = new SideSheetBehavior<>(); assertThat(sideSheetBehavior.getState()).isEqualTo(STATE_HIDDEN); hideSheet(sideSheetBehavior); @@ -92,6 +104,79 @@ public void hide_ofHiddenSheet_isIdempotent() { assertThat(sideSheetBehavior.getState()).isEqualTo(STATE_HIDDEN); } + @Test + public void onInitialization_sheetIsInvisible() { + assertThat(sideSheet.getVisibility()).isEqualTo(View.INVISIBLE); + } + + @Test + public void show_ofHiddenSheet_sheetIsVisible() { + expandSheet(sideSheetBehavior); + + assertThat(sideSheet.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void hide_ofExpandedSheet_sheetIsInvisible() { + expandSheet(sideSheetBehavior); + hideSheet(sideSheetBehavior); + + assertThat(sideSheet.getVisibility()).isEqualTo(View.INVISIBLE); + } + + @Test + public void drag_ofExpandedSheet_sheetIsVisible() { + expandSheet(sideSheetBehavior); + + sideSheetBehavior.setStateInternal(STATE_DRAGGING); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(sideSheet.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void settle_ofHiddenSheet_sheetIsVisible() { + // Sheet is hidden on initialization. + sideSheetBehavior.setStateInternal(STATE_SETTLING); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(sideSheet.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void settle_ofExpandedSheet_sheetIsVisible() { + expandSheet(sideSheetBehavior); + + sideSheetBehavior.setStateInternal(STATE_SETTLING); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(sideSheet.getVisibility()).isEqualTo(View.VISIBLE); + } + + @TargetApi(VERSION_CODES.KITKAT) + @Test + public void setAccessibilityPaneTitle_ofDefaultSheet_customTitleIsUsed() { + if (VERSION.SDK_INT < VERSION_CODES.KITKAT) { + return; + } + String defaultAccessibilityPaneTitle = + String.valueOf(ViewCompat.getAccessibilityPaneTitle(sideSheet)); + shadowOf(Looper.getMainLooper()).idle(); + + String customAccessibilityPaneTitle = "Custom side sheet accessibility pane title"; + + ViewCompat.setAccessibilityPaneTitle(sideSheet, customAccessibilityPaneTitle); + shadowOf(Looper.getMainLooper()).idle(); + + String updatedAccessibilityPaneTitle = + String.valueOf(ViewCompat.getAccessibilityPaneTitle(sideSheet)); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(defaultAccessibilityPaneTitle).isNotNull(); + assertThat(updatedAccessibilityPaneTitle).isEqualTo(customAccessibilityPaneTitle); + assertThat(updatedAccessibilityPaneTitle).isNotEqualTo(defaultAccessibilityPaneTitle); + } + private void expandSheet(SideSheetBehavior sideSheetBehavior) { sideSheetBehavior.expand(); shadowOf(Looper.getMainLooper()).idle();