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); } }