Skip to content

Commit

Permalink
[AppBarLayout] Fix scrolling for a11y
Browse files Browse the repository at this point in the history
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 72228f4)
  • Loading branch information
Material Design Team authored and pekingme committed May 25, 2022
1 parent 248d4c8 commit a5a738b
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 7 deletions.
56 changes: 50 additions & 6 deletions lib/java/com/google/android/material/appbar/AppBarLayout.java
Expand Up @@ -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;
Expand Down Expand Up @@ -1368,6 +1370,8 @@ public abstract static class BaseDragCallback<T extends AppBarLayout> {
@Nullable private WeakReference<View> lastNestedScrollingChildRef;
private BaseDragCallback onDragCallback;

private boolean coordinatorLayoutA11yScrollable;

public BaseBehavior() {}

public BaseBehavior(Context context, AttributeSet attrs) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
}

Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
}
Expand Up @@ -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];
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -155,6 +157,7 @@ public void testPinnedToolbar() throws Throwable {
assertScrimAlpha(0);
assertAccessibilityHasScrollForwardAction(true);
assertAccessibilityHasScrollBackwardAction(false);
assertAccessibilityScrollable(true);
}

@Test
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -306,13 +308,16 @@ public void testUpdateAccessibilityActionWithViewsRemoved() throws Throwable {

assertAccessibilityHasScrollForwardAction(true);
assertAccessibilityHasScrollBackwardAction(false);
assertAccessibilityScrollable(true);

activityTestRule.runOnUiThread(
() -> {
mCoordinatorLayout.removeAllViews();
});

assertAccessibilityHasScrollForwardAction(false);
assertAccessibilityHasScrollBackwardAction(false);
assertAccessibilityScrollable(false);
}

@Test
Expand All @@ -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<AppBarLayout> behavior =
((CoordinatorLayout.LayoutParams) mAppBar.getLayoutParams()).getBehavior();
behavior.onLayoutChild(mCoordinatorLayout, mAppBar, mAppBar.getLayoutDirection());
});

assertAccessibilityHasScrollForwardAction(false);
assertAccessibilityHasScrollBackwardAction(false);

Expand All @@ -344,10 +353,14 @@ public void testUpdateAccessibilityActionsWithSetScrollFlags() throws Throwable
activityTestRule.runOnUiThread(
() -> {
mToolbar.setLayoutParams(lp);
final CoordinatorLayout.Behavior<AppBarLayout> 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.
Expand All @@ -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);
}
}

0 comments on commit a5a738b

Please sign in to comment.