Skip to content

Commit 0a03911

Browse files
Material Design Teampekingme
authored andcommitted
Add more a11y support to AppBarLayout's BaseBehavior.
PiperOrigin-RevId: 289692654
1 parent 0e6c21d commit 0a03911

File tree

4 files changed

+280
-1
lines changed

4 files changed

+280
-1
lines changed

lib/java/com/google/android/material/appbar/AppBarLayout.java

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import com.google.android.material.R;
2020

2121
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
22+
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_BACKWARD;
23+
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_FORWARD;
2224

2325
import android.animation.ValueAnimator;
2426
import android.animation.ValueAnimator.AnimatorUpdateListener;
@@ -50,6 +52,8 @@
5052
import androidx.core.view.ViewCompat;
5153
import androidx.core.view.ViewCompat.NestedScrollType;
5254
import androidx.core.view.WindowInsetsCompat;
55+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
56+
import androidx.core.view.accessibility.AccessibilityViewCommand;
5357
import androidx.appcompat.content.res.AppCompatResources;
5458
import android.util.AttributeSet;
5559
import android.view.View;
@@ -1020,7 +1024,6 @@ public static class LayoutParams extends LinearLayout.LayoutParams {
10201024
})
10211025
@Retention(RetentionPolicy.SOURCE)
10221026
public @interface ScrollFlags {}
1023-
10241027
/**
10251028
* Disable scrolling on the view. This flag should not be combined with any of the other scroll
10261029
* flags.
@@ -1324,6 +1327,12 @@ public void onNestedScroll(
13241327
consumed[1] =
13251328
scroll(coordinatorLayout, child, dyUnconsumed, -child.getDownNestedScrollRange(), 0);
13261329
}
1330+
1331+
if (dyUnconsumed == 0) {
1332+
// The scrolling view may scroll to the top of its content without updating the actions, so
1333+
// update here.
1334+
updateAccessibilityActions(coordinatorLayout, child);
1335+
}
13271336
}
13281337

13291338
@Override
@@ -1560,9 +1569,94 @@ public boolean onLayoutChild(
15601569
// Make sure we dispatch the offset update
15611570
abl.onOffsetChanged(getTopAndBottomOffset());
15621571

1572+
updateAccessibilityActions(parent, abl);
15631573
return handled;
15641574
}
15651575

1576+
private void updateAccessibilityActions(
1577+
CoordinatorLayout coordinatorLayout, @NonNull T appBarLayout) {
1578+
ViewCompat.removeAccessibilityAction(coordinatorLayout, ACTION_SCROLL_FORWARD.getId());
1579+
ViewCompat.removeAccessibilityAction(coordinatorLayout, ACTION_SCROLL_BACKWARD.getId());
1580+
View scrollingView = findFirstScrollingChild(coordinatorLayout);
1581+
// Don't add a11y actions if there is no scrolling view that the abl depends on for scrolling
1582+
// or the abl has no scroll range.
1583+
if (scrollingView == null || appBarLayout.getTotalScrollRange() == 0) {
1584+
return;
1585+
}
1586+
// Don't add actions if the scrolling view doesn't have the behavior that will cause the abl
1587+
// to scroll.
1588+
CoordinatorLayout.LayoutParams lp =
1589+
(CoordinatorLayout.LayoutParams) scrollingView.getLayoutParams();
1590+
if (!(lp.getBehavior() instanceof ScrollingViewBehavior)) {
1591+
return;
1592+
}
1593+
addAccessibilityScrollActions(coordinatorLayout, appBarLayout, scrollingView);
1594+
}
1595+
1596+
private void addAccessibilityScrollActions(
1597+
final CoordinatorLayout coordinatorLayout,
1598+
@NonNull final T appBarLayout,
1599+
@NonNull final View scrollingView) {
1600+
if (getTopBottomOffsetForScrollingSibling() != -appBarLayout.getTotalScrollRange()
1601+
&& scrollingView.canScrollVertically(1)) {
1602+
// Add a collapsing action if the view can scroll up and the offset isn't the abl scroll
1603+
// range. (This offset means the view is completely collapsed). Collapse to minimum height.
1604+
addActionToExpand(coordinatorLayout, appBarLayout, ACTION_SCROLL_FORWARD, false);
1605+
}
1606+
// Don't add an expanding action if the sibling offset is 0, which would mean the abl is
1607+
// completely expanded.
1608+
if (getTopBottomOffsetForScrollingSibling() != 0) {
1609+
if (scrollingView.canScrollVertically(-1)) {
1610+
// Expanding action. If the view can scroll down, expand the app bar reflecting the logic
1611+
// in onNestedPreScroll.
1612+
final int dy = -appBarLayout.getDownNestedPreScrollRange();
1613+
// Offset by non-zero.
1614+
if (dy != 0) {
1615+
ViewCompat.replaceAccessibilityAction(
1616+
coordinatorLayout,
1617+
ACTION_SCROLL_BACKWARD,
1618+
null,
1619+
new AccessibilityViewCommand() {
1620+
@Override
1621+
public boolean perform(@NonNull View view, @Nullable CommandArguments arguments) {
1622+
onNestedPreScroll(
1623+
coordinatorLayout,
1624+
appBarLayout,
1625+
scrollingView,
1626+
0,
1627+
dy,
1628+
new int[] {0, 0},
1629+
ViewCompat.TYPE_NON_TOUCH);
1630+
return true;
1631+
}
1632+
});
1633+
}
1634+
} else {
1635+
// If the view can't scroll down, we are probably at the top of the scrolling content so
1636+
// expand completely.
1637+
addActionToExpand(coordinatorLayout, appBarLayout, ACTION_SCROLL_BACKWARD, true);
1638+
}
1639+
}
1640+
}
1641+
1642+
private void addActionToExpand(
1643+
CoordinatorLayout parent,
1644+
@NonNull final T appBarLayout,
1645+
@NonNull AccessibilityActionCompat action,
1646+
final boolean expand) {
1647+
ViewCompat.replaceAccessibilityAction(
1648+
parent,
1649+
action,
1650+
null,
1651+
new AccessibilityViewCommand() {
1652+
@Override
1653+
public boolean perform(@NonNull View view, @Nullable CommandArguments arguments) {
1654+
appBarLayout.setExpanded(expand);
1655+
return true;
1656+
}
1657+
});
1658+
}
1659+
15661660
@Override
15671661
boolean canDragView(T view) {
15681662
if (onDragCallback != null) {
@@ -1653,6 +1747,7 @@ int setHeaderTopBottomOffset(
16531747
offsetDelta = 0;
16541748
}
16551749

1750+
updateAccessibilityActions(coordinatorLayout, appBarLayout);
16561751
return consumed;
16571752
}
16581753

@@ -1922,6 +2017,15 @@ public boolean onDependentViewChanged(
19222017
return false;
19232018
}
19242019

2020+
@Override
2021+
public void onDependentViewRemoved(
2022+
@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
2023+
if (dependency instanceof AppBarLayout) {
2024+
ViewCompat.removeAccessibilityAction(parent, ACTION_SCROLL_FORWARD.getId());
2025+
ViewCompat.removeAccessibilityAction(parent, ACTION_SCROLL_BACKWARD.getId());
2026+
}
2027+
}
2028+
19252029
@Override
19262030
public boolean onRequestChildRectangleOnScreen(
19272031
@NonNull CoordinatorLayout parent,

tests/javatests/com/google/android/material/appbar/AppBarLayoutBaseTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,21 @@
2525
import static com.google.android.material.testutils.SwipeUtils.swipeUp;
2626
import static com.google.android.material.testutils.TestUtilsActions.setText;
2727
import static com.google.android.material.testutils.TestUtilsActions.setTitle;
28+
import static org.hamcrest.CoreMatchers.equalTo;
2829
import static org.junit.Assert.assertEquals;
30+
import static org.junit.Assert.assertThat;
2931

3032
import android.graphics.Color;
3133
import android.os.Build;
34+
import android.os.Build.VERSION;
3235
import android.os.SystemClock;
3336
import androidx.annotation.CallSuper;
3437
import androidx.annotation.IdRes;
3538
import androidx.annotation.IntRange;
3639
import androidx.annotation.LayoutRes;
3740
import androidx.annotation.StringRes;
3841
import androidx.core.view.ViewCompat;
42+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
3943
import androidx.appcompat.app.AppCompatActivity;
4044
import androidx.appcompat.widget.Toolbar;
4145
import android.text.TextUtils;
@@ -44,6 +48,7 @@
4448
import com.google.android.material.internal.BaseDynamicCoordinatorLayoutTest;
4549
import com.google.android.material.resources.TextAppearanceConfig;
4650
import com.google.android.material.testapp.R;
51+
import com.google.android.material.testutils.AccessibilityUtils;
4752
import com.google.android.material.testutils.Shakespeare;
4853
import org.hamcrest.Description;
4954
import org.hamcrest.Matcher;
@@ -127,4 +132,22 @@ protected boolean matchesSafely(View view) {
127132
}
128133
};
129134
}
135+
136+
protected void assertAccessibilityHasScrollForwardAction(boolean hasScrollForward) {
137+
if (VERSION.SDK_INT >= 21) {
138+
assertThat(
139+
AccessibilityUtils.hasAction(
140+
mCoordinatorLayout, AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD),
141+
equalTo(hasScrollForward));
142+
}
143+
}
144+
145+
protected void assertAccessibilityHasScrollBackwardAction(boolean hasScrollBackward) {
146+
if (VERSION.SDK_INT >= 21) {
147+
assertThat(
148+
AccessibilityUtils.hasAction(
149+
mCoordinatorLayout, AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD),
150+
equalTo(hasScrollBackward));
151+
}
152+
}
130153
}

tests/javatests/com/google/android/material/appbar/AppBarWithCollapsingToolbarTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import android.os.Build;
2525
import android.widget.ImageView;
26+
import androidx.coordinatorlayout.widget.CoordinatorLayout;
2627
import androidx.test.filters.LargeTest;
2728
import androidx.test.filters.SdkSuppress;
2829
import androidx.test.runner.AndroidJUnit4;
@@ -34,6 +35,7 @@
3435

3536
@LargeTest
3637
@RunWith(AndroidJUnit4.class)
38+
@SuppressWarnings("unchecked")
3739
public class AppBarWithCollapsingToolbarTest extends AppBarLayoutBaseTest {
3840
@Test
3941
// Suppressed due to high % flakiness on API 15
@@ -47,6 +49,16 @@ public void testPinnedToolbar() throws Throwable {
4749
assertEquals(
4850
CollapsingToolbarLayout.LayoutParams.COLLAPSE_MODE_PIN, toolbarLp.getCollapseMode());
4951

52+
// Call onLayout so the accessibility actions are initially updated.
53+
activityTestRule.runOnUiThread(
54+
() -> {
55+
final CoordinatorLayout.Behavior<AppBarLayout> behavior =
56+
((CoordinatorLayout.LayoutParams) mAppBar.getLayoutParams()).getBehavior();
57+
behavior.onLayoutChild(mCoordinatorLayout, mAppBar, mAppBar.getLayoutDirection());
58+
});
59+
assertAccessibilityHasScrollForwardAction(true);
60+
assertAccessibilityHasScrollBackwardAction(false);
61+
5062
final int[] appbarOnScreenXY = new int[2];
5163
final int[] coordinatorLayoutOnScreenXY = new int[2];
5264
mAppBar.getLocationOnScreen(appbarOnScreenXY);
@@ -72,6 +84,10 @@ public void testPinnedToolbar() throws Throwable {
7284
originalAppbarBottom + longSwipeAmount / 2,
7385
longSwipeAmount);
7486

87+
// Content is already collapsed, so it can't scroll forward. The pre-scroll range will be 0
88+
// for SCROLL_FLAG_SCROLL and SCROLL_EXIT_UNTIL_COLLAPSED, so it can't scroll backward.
89+
assertAccessibilityHasScrollForwardAction(false);
90+
assertAccessibilityHasScrollBackwardAction(false);
7591
mAppBar.getLocationOnScreen(appbarOnScreenXY);
7692
// At this point the app bar should be visually snapped below the system status bar.
7793
// Allow for off-by-a-pixel margin of error.
@@ -137,6 +153,8 @@ public void testPinnedToolbar() throws Throwable {
137153
assertEquals(originalAppbarBottom, appbarOnScreenXY[1] + appbarHeight, 1);
138154
assertAppBarElevation(0f);
139155
assertScrimAlpha(0);
156+
assertAccessibilityHasScrollForwardAction(true);
157+
assertAccessibilityHasScrollBackwardAction(false);
140158
}
141159

142160
@Test
@@ -172,11 +190,26 @@ public void testScrollingToolbar() throws Throwable {
172190
assertAppBarElevation(0f);
173191
assertScrimAlpha(0);
174192

193+
// Call onLayout so the accessibility actions are initially updated.
194+
activityTestRule.runOnUiThread(
195+
() -> {
196+
final CoordinatorLayout.Behavior<AppBarLayout> behavior =
197+
((CoordinatorLayout.LayoutParams) mAppBar.getLayoutParams()).getBehavior();
198+
behavior.onLayoutChild(mCoordinatorLayout, mAppBar, mAppBar.getLayoutDirection());
199+
});
200+
assertAccessibilityHasScrollForwardAction(true);
201+
assertAccessibilityHasScrollBackwardAction(false);
202+
175203
// Perform a swipe-up gesture across the horizontal center of the screen, starting from
176204
// just below the AppBarLayout
177205
performVerticalSwipeUpGesture(
178206
R.id.coordinator_layout, centerX, originalAppbarBottom + 20, longSwipeAmount);
179207

208+
// Bar is collapsed. With SCROLL_ENTER_ALWAYS the bar expands immediately on any scroll and thus
209+
// has a scroll backward action.
210+
assertAccessibilityHasScrollForwardAction(false);
211+
assertAccessibilityHasScrollBackwardAction(true);
212+
180213
mAppBar.getLocationOnScreen(appbarOnScreenXY);
181214
// At this point the app bar should not be visually "present" on the screen, with its bottom
182215
// edge aligned with the bottom of system status bar. If we're running on a device which
@@ -223,6 +256,8 @@ public void testScrollingToolbar() throws Throwable {
223256
assertEquals(originalAppbarBottom, appbarOnScreenXY[1] + appbarHeight);
224257
assertAppBarElevation(0f);
225258
assertScrimAlpha(0);
259+
assertAccessibilityHasScrollForwardAction(true);
260+
assertAccessibilityHasScrollBackwardAction(false);
226261

227262
// Perform yet another swipe-down gesture across the horizontal center of the screen.
228263
performVerticalSwipeDownGesture(

0 commit comments

Comments
 (0)