From a5a738bfa2f2786e8d6bd91e77d823d3534612b1 Mon Sep 17 00:00:00 2001 From: Material Design Team Date: Mon, 9 May 2022 19:57:11 -0400 Subject: [PATCH] [AppBarLayout] Fix scrolling for a11y Ensure the CoL can scroll to the bottommost elements of the scrolling child, which may not be visible due to the app bar. Also, set the node info properties to expose the CoL to auto-scrolling. PiperOrigin-RevId: 447596411 (cherry picked from commit 72228f4f35b0c155c870f59bc7c807e05443bf95) --- .../android/material/appbar/AppBarLayout.java | 56 +++++++++++++++++-- .../material/appbar/AppBarLayoutBaseTest.java | 12 ++++ .../AppBarWithCollapsingToolbarTest.java | 6 ++ .../appbar/AppBarWithToolbarTest.java | 16 +++++- 4 files changed, 83 insertions(+), 7 deletions(-) diff --git a/lib/java/com/google/android/material/appbar/AppBarLayout.java b/lib/java/com/google/android/material/appbar/AppBarLayout.java index f775bb5f4f0..3b60de07e17 100644 --- a/lib/java/com/google/android/material/appbar/AppBarLayout.java +++ b/lib/java/com/google/android/material/appbar/AppBarLayout.java @@ -60,10 +60,12 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.util.ObjectsCompat; +import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.NestedScrollingChild; import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat.NestedScrollType; import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; import androidx.core.view.accessibility.AccessibilityViewCommand; import androidx.customview.view.AbsSavedState; @@ -1368,6 +1370,8 @@ public abstract static class BaseDragCallback { @Nullable private WeakReference lastNestedScrollingChildRef; private BaseDragCallback onDragCallback; + private boolean coordinatorLayoutA11yScrollable; + public BaseBehavior() {} public BaseBehavior(Context context, AttributeSet attrs) { @@ -1741,18 +1745,54 @@ private void updateAccessibilityActions( if (!(lp.getBehavior() instanceof ScrollingViewBehavior)) { return; } - addAccessibilityScrollActions(coordinatorLayout, appBarLayout, scrollingView); + + // Don't add actions if the children do not have scrolling flags. + if (!childrenHaveScrollFlags(appBarLayout)) { + return; + } + + if (!ViewCompat.hasAccessibilityDelegate(coordinatorLayout)) { + ViewCompat.setAccessibilityDelegate( + coordinatorLayout, + new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityNodeInfo( + View host, @NonNull AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setScrollable(coordinatorLayoutA11yScrollable); + info.setClassName(ScrollView.class.getName()); + } + }); + } + + coordinatorLayoutA11yScrollable = + addAccessibilityScrollActions(coordinatorLayout, appBarLayout, scrollingView); + } + + private boolean childrenHaveScrollFlags(AppBarLayout appBarLayout) { + final int childCount = appBarLayout.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = appBarLayout.getChildAt(i); + final LayoutParams childLp = (LayoutParams) child.getLayoutParams(); + final int flags = childLp.scrollFlags; + + if (flags != AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL) { + return true; + } + } + return false; } - private void addAccessibilityScrollActions( + private boolean addAccessibilityScrollActions( final CoordinatorLayout coordinatorLayout, @NonNull final T appBarLayout, @NonNull final View scrollingView) { - if (getTopBottomOffsetForScrollingSibling() != -appBarLayout.getTotalScrollRange() - && scrollingView.canScrollVertically(1)) { - // Add a collapsing action if the view can scroll up and the offset isn't the abl scroll - // range. (This offset means the view is completely collapsed). Collapse to minimum height. + boolean a11yScrollable = false; + if (getTopBottomOffsetForScrollingSibling() != -appBarLayout.getTotalScrollRange()) { + // Add a collapsing action if the view offset isn't the abl scroll range. + // (The same offset means the view is completely collapsed). Collapse to minimum height. addActionToExpand(coordinatorLayout, appBarLayout, ACTION_SCROLL_FORWARD, false); + a11yScrollable = true; } // Don't add an expanding action if the sibling offset is 0, which would mean the abl is // completely expanded. @@ -1781,13 +1821,16 @@ public boolean perform(@NonNull View view, @Nullable CommandArguments arguments) return true; } }); + a11yScrollable = true; } } else { // If the view can't scroll down, we are probably at the top of the scrolling content so // expand completely. addActionToExpand(coordinatorLayout, appBarLayout, ACTION_SCROLL_BACKWARD, true); + a11yScrollable = true; } } + return a11yScrollable; } private void addActionToExpand( @@ -2203,6 +2246,7 @@ public void onDependentViewRemoved( if (dependency instanceof AppBarLayout) { ViewCompat.removeAccessibilityAction(parent, ACTION_SCROLL_FORWARD.getId()); ViewCompat.removeAccessibilityAction(parent, ACTION_SCROLL_BACKWARD.getId()); + ViewCompat.setAccessibilityDelegate(parent, null); } } diff --git a/tests/javatests/com/google/android/material/appbar/AppBarLayoutBaseTest.java b/tests/javatests/com/google/android/material/appbar/AppBarLayoutBaseTest.java index 87c19f808d0..2d9efc1188d 100644 --- a/tests/javatests/com/google/android/material/appbar/AppBarLayoutBaseTest.java +++ b/tests/javatests/com/google/android/material/appbar/AppBarLayoutBaseTest.java @@ -27,7 +27,9 @@ import static com.google.android.material.testutils.TestUtilsActions.setTitle; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import android.graphics.Color; import android.os.Build; @@ -148,4 +150,14 @@ protected void assertAccessibilityHasScrollBackwardAction(boolean hasScrollBackw equalTo(hasScrollBackward)); } } + + protected void assertAccessibilityScrollable(boolean isScrollable) { + AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(); + ViewCompat.onInitializeAccessibilityNodeInfo(mCoordinatorLayout, info); + if (isScrollable) { + assertTrue(info.isScrollable()); + } else { + assertFalse(info.isScrollable()); + } + } } diff --git a/tests/javatests/com/google/android/material/appbar/AppBarWithCollapsingToolbarTest.java b/tests/javatests/com/google/android/material/appbar/AppBarWithCollapsingToolbarTest.java index 6422e9b9ca2..41145dad817 100644 --- a/tests/javatests/com/google/android/material/appbar/AppBarWithCollapsingToolbarTest.java +++ b/tests/javatests/com/google/android/material/appbar/AppBarWithCollapsingToolbarTest.java @@ -58,6 +58,7 @@ public void testPinnedToolbar() throws Throwable { }); assertAccessibilityHasScrollForwardAction(true); assertAccessibilityHasScrollBackwardAction(false); + assertAccessibilityScrollable(true); final int[] appbarOnScreenXY = new int[2]; final int[] coordinatorLayoutOnScreenXY = new int[2]; @@ -88,6 +89,7 @@ public void testPinnedToolbar() throws Throwable { // for SCROLL_FLAG_SCROLL and SCROLL_EXIT_UNTIL_COLLAPSED, so it can't scroll backward. assertAccessibilityHasScrollForwardAction(false); assertAccessibilityHasScrollBackwardAction(false); + assertAccessibilityScrollable(false); mAppBar.getLocationOnScreen(appbarOnScreenXY); // At this point the app bar should be visually snapped below the system status bar. // Allow for off-by-a-pixel margin of error. @@ -155,6 +157,7 @@ public void testPinnedToolbar() throws Throwable { assertScrimAlpha(0); assertAccessibilityHasScrollForwardAction(true); assertAccessibilityHasScrollBackwardAction(false); + assertAccessibilityScrollable(true); } @Test @@ -199,6 +202,7 @@ public void testScrollingToolbar() throws Throwable { }); assertAccessibilityHasScrollForwardAction(true); assertAccessibilityHasScrollBackwardAction(false); + assertAccessibilityScrollable(true); // Perform a swipe-up gesture across the horizontal center of the screen, starting from // just below the AppBarLayout @@ -209,6 +213,7 @@ public void testScrollingToolbar() throws Throwable { // has a scroll backward action. assertAccessibilityHasScrollForwardAction(false); assertAccessibilityHasScrollBackwardAction(true); + assertAccessibilityScrollable(true); mAppBar.getLocationOnScreen(appbarOnScreenXY); // At this point the app bar should not be visually "present" on the screen, with its bottom @@ -258,6 +263,7 @@ public void testScrollingToolbar() throws Throwable { assertScrimAlpha(0); assertAccessibilityHasScrollForwardAction(true); assertAccessibilityHasScrollBackwardAction(false); + assertAccessibilityScrollable(true); // Perform yet another swipe-down gesture across the horizontal center of the screen. performVerticalSwipeDownGesture( diff --git a/tests/javatests/com/google/android/material/appbar/AppBarWithToolbarTest.java b/tests/javatests/com/google/android/material/appbar/AppBarWithToolbarTest.java index 309c10f1e2e..003a4c4c1d7 100644 --- a/tests/javatests/com/google/android/material/appbar/AppBarWithToolbarTest.java +++ b/tests/javatests/com/google/android/material/appbar/AppBarWithToolbarTest.java @@ -275,6 +275,7 @@ public void testUpdateAccessibilityActionsWithEnterAlwaysFlag() throws Throwable // Very top of screen, can scroll forward to collapse but can't scroll backward. assertAccessibilityHasScrollForwardAction(true); assertAccessibilityHasScrollBackwardAction(false); + assertAccessibilityScrollable(true); // Perform a swipe-up gesture across the horizontal center of the screen. performVerticalSwipeUpGesture( @@ -287,6 +288,7 @@ public void testUpdateAccessibilityActionsWithEnterAlwaysFlag() throws Throwable // the bar will always be entered/expanded on scroll. assertAccessibilityHasScrollForwardAction(false); assertAccessibilityHasScrollBackwardAction(true); + assertAccessibilityScrollable(true); } @Test @@ -306,6 +308,8 @@ public void testUpdateAccessibilityActionWithViewsRemoved() throws Throwable { assertAccessibilityHasScrollForwardAction(true); assertAccessibilityHasScrollBackwardAction(false); + assertAccessibilityScrollable(true); + activityTestRule.runOnUiThread( () -> { mCoordinatorLayout.removeAllViews(); @@ -313,6 +317,7 @@ public void testUpdateAccessibilityActionWithViewsRemoved() throws Throwable { assertAccessibilityHasScrollForwardAction(false); assertAccessibilityHasScrollBackwardAction(false); + assertAccessibilityScrollable(false); } @Test @@ -330,12 +335,16 @@ public void testUpdateAccessibilityActionsWithSetScrollFlags() throws Throwable AppBarLayout.LayoutParams lp = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams(); - // Disable scrolling and update the a11y actions. + // Disable scrolling and call onLayout to update the a11y actions. lp.setScrollFlags(SCROLL_FLAG_NO_SCROLL); activityTestRule.runOnUiThread( () -> { mToolbar.setLayoutParams(lp); + final CoordinatorLayout.Behavior behavior = + ((CoordinatorLayout.LayoutParams) mAppBar.getLayoutParams()).getBehavior(); + behavior.onLayoutChild(mCoordinatorLayout, mAppBar, mAppBar.getLayoutDirection()); }); + assertAccessibilityHasScrollForwardAction(false); assertAccessibilityHasScrollBackwardAction(false); @@ -344,10 +353,14 @@ public void testUpdateAccessibilityActionsWithSetScrollFlags() throws Throwable activityTestRule.runOnUiThread( () -> { mToolbar.setLayoutParams(lp); + final CoordinatorLayout.Behavior behavior = + ((CoordinatorLayout.LayoutParams) mAppBar.getLayoutParams()).getBehavior(); + behavior.onLayoutChild(mCoordinatorLayout, mAppBar, mAppBar.getLayoutDirection()); }); // Can scroll forward to collapse, and cannot expand because it's already expanded. assertAccessibilityHasScrollForwardAction(true); assertAccessibilityHasScrollBackwardAction(false); + assertAccessibilityScrollable(true); // Perform a swipe-up gesture across the horizontal center of the screen. The toolbar should be // collapsed. @@ -361,5 +374,6 @@ public void testUpdateAccessibilityActionsWithSetScrollFlags() throws Throwable // for SCROLL_FLAG_SCROLL, so it can't scroll backward. assertAccessibilityHasScrollForwardAction(false); assertAccessibilityHasScrollBackwardAction(false); + assertAccessibilityScrollable(false); } }