Skip to content

Commit 929c80f

Browse files
Material Design Teamhunterstich
authored andcommitted
[AppBarLayout] Use an accessibility delegate to add and perform actions
This replaces the use of `ViewCompat#add/removeAccessibilityAction` Both are valid strategies, but each call to `remove` or `add` triggers an accessibility event. Since this check is done in layout (originally to fix a11y scroll state) it's sending a high number of events that create noise for accessibility services. To avoid this, we move this code to the delegate `onInitialize` and `performAction` methods. Instead of the view dynamically adding and removing actions to itself, the node is initialized with actions only when an a11y service sends a request with a new node (likely due to some UI change). The flow here would look like: 1. UI is scrolled/page is loaded 2. TalkBack gets a scroll event/content change event 3. TalkBack requests new snapshot of the screen 4. ABL populates the node with the actions For a simple scroll, this change reduces the events from ~40 to ~10. We also add the Truth library for clearer assertions. PiperOrigin-RevId: 605333170 (cherry picked from commit 8a71e77)
1 parent 5efdae3 commit 929c80f

File tree

2 files changed

+96
-115
lines changed

2 files changed

+96
-115
lines changed

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

Lines changed: 82 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import android.os.Build;
3939
import android.os.Build.VERSION;
4040
import android.os.Build.VERSION_CODES;
41+
import android.os.Bundle;
4142
import android.os.Parcel;
4243
import android.os.Parcelable;
4344
import androidx.appcompat.content.res.AppCompatResources;
@@ -68,8 +69,6 @@
6869
import androidx.core.view.ViewCompat.NestedScrollType;
6970
import androidx.core.view.WindowInsetsCompat;
7071
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
71-
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
72-
import androidx.core.view.accessibility.AccessibilityViewCommand;
7372
import androidx.customview.view.AbsSavedState;
7473
import com.google.android.material.animation.AnimationUtils;
7574
import com.google.android.material.appbar.AppBarLayout.BaseBehavior.SavedState;
@@ -1516,8 +1515,6 @@ public abstract static class BaseDragCallback<T extends AppBarLayout> {
15161515
@Nullable private WeakReference<View> lastNestedScrollingChildRef;
15171516
private BaseDragCallback onDragCallback;
15181517

1519-
private boolean coordinatorLayoutA11yScrollable;
1520-
15211518
public BaseBehavior() {}
15221519

15231520
public BaseBehavior(Context context, AttributeSet attrs) {
@@ -1610,7 +1607,7 @@ public void onNestedScroll(
16101607
if (dyUnconsumed == 0) {
16111608
// The scrolling view may scroll to the top of its content without updating the actions, so
16121609
// update here.
1613-
updateAccessibilityActions(coordinatorLayout, child);
1610+
addAccessibilityDelegateIfNeeded(coordinatorLayout, child);
16141611
}
16151612
}
16161613

@@ -1870,30 +1867,12 @@ public boolean onLayoutChild(
18701867
// Make sure we dispatch the offset update
18711868
abl.onOffsetChanged(getTopAndBottomOffset());
18721869

1873-
updateAccessibilityActions(parent, abl);
1870+
addAccessibilityDelegateIfNeeded(parent, abl);
18741871
return handled;
18751872
}
18761873

1877-
private void updateAccessibilityActions(
1874+
private void addAccessibilityDelegateIfNeeded(
18781875
CoordinatorLayout coordinatorLayout, @NonNull T appBarLayout) {
1879-
ViewCompat.removeAccessibilityAction(coordinatorLayout, ACTION_SCROLL_FORWARD.getId());
1880-
ViewCompat.removeAccessibilityAction(coordinatorLayout, ACTION_SCROLL_BACKWARD.getId());
1881-
// Don't add a11y actions if the abl has no scroll range.
1882-
if (appBarLayout.getTotalScrollRange() == 0) {
1883-
return;
1884-
}
1885-
// Don't add actions if a child view doesn't have the behavior that will cause the abl to
1886-
// scroll.
1887-
View scrollingView = getChildWithScrollingBehavior(coordinatorLayout);
1888-
if (scrollingView == null) {
1889-
return;
1890-
}
1891-
1892-
// Don't add actions if the children do not have scrolling flags.
1893-
if (!childrenHaveScrollFlags(appBarLayout)) {
1894-
return;
1895-
}
1896-
18971876
if (!ViewCompat.hasAccessibilityDelegate(coordinatorLayout)) {
18981877
ViewCompat.setAccessibilityDelegate(
18991878
coordinatorLayout,
@@ -1902,14 +1881,87 @@ private void updateAccessibilityActions(
19021881
public void onInitializeAccessibilityNodeInfo(
19031882
View host, @NonNull AccessibilityNodeInfoCompat info) {
19041883
super.onInitializeAccessibilityNodeInfo(host, info);
1905-
info.setScrollable(coordinatorLayoutA11yScrollable);
19061884
info.setClassName(ScrollView.class.getName());
1885+
if (appBarLayout.getTotalScrollRange() == 0) {
1886+
return;
1887+
}
1888+
View scrollingView = getChildWithScrollingBehavior(coordinatorLayout);
1889+
// Don't add actions if a child view doesn't have the behavior that will cause the
1890+
// ABL to scroll.
1891+
if (scrollingView == null) {
1892+
return;
1893+
}
1894+
1895+
// Don't add actions if the children do not have scrolling flags.
1896+
if (!childrenHaveScrollFlags(appBarLayout)) {
1897+
return;
1898+
}
1899+
1900+
if (getTopBottomOffsetForScrollingSibling()
1901+
!= -appBarLayout.getTotalScrollRange()) {
1902+
// Add a collapsing action/forward if the view offset isn't the ABL scroll range.
1903+
// (The same offset means the view is completely collapsed).
1904+
info.addAction(ACTION_SCROLL_FORWARD);
1905+
info.setScrollable(true);
1906+
}
1907+
1908+
// Don't add an expanding action if the sibling offset is 0, which would mean the
1909+
// ABL is completely expanded.
1910+
if (getTopBottomOffsetForScrollingSibling() != 0) {
1911+
if (scrollingView.canScrollVertically(-1)) {
1912+
final int dy = -appBarLayout.getDownNestedPreScrollRange();
1913+
// Offset by non-zero.
1914+
if (dy != 0) {
1915+
info.addAction(ACTION_SCROLL_BACKWARD);
1916+
info.setScrollable(true);
1917+
}
1918+
} else {
1919+
info.addAction(ACTION_SCROLL_BACKWARD);
1920+
info.setScrollable(true);
1921+
}
1922+
}
1923+
}
1924+
1925+
@Override
1926+
public boolean performAccessibilityAction(View host, int action, Bundle args) {
1927+
1928+
if (action == AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD) {
1929+
appBarLayout.setExpanded(false);
1930+
return true;
1931+
} else if (action == AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD) {
1932+
if (getTopBottomOffsetForScrollingSibling() != 0) {
1933+
View scrollingView = getChildWithScrollingBehavior(coordinatorLayout);
1934+
if (scrollingView.canScrollVertically(-1)) {
1935+
// Expanding action. If the view can scroll down, expand the app bar
1936+
// reflecting the logic
1937+
// in onNestedPreScroll.
1938+
final int dy = -appBarLayout.getDownNestedPreScrollRange();
1939+
// Offset by non-zero.
1940+
if (dy != 0) {
1941+
onNestedPreScroll(
1942+
coordinatorLayout,
1943+
appBarLayout,
1944+
scrollingView,
1945+
0,
1946+
dy,
1947+
new int[] {0, 0},
1948+
ViewCompat.TYPE_NON_TOUCH);
1949+
return true;
1950+
}
1951+
} else {
1952+
// If the view can't scroll down, we are probably at the top of the
1953+
// scrolling content so expand completely.
1954+
appBarLayout.setExpanded(true);
1955+
return true;
1956+
}
1957+
}
1958+
} else {
1959+
return super.performAccessibilityAction(host, action, args);
1960+
}
1961+
return false;
19071962
}
19081963
});
19091964
}
1910-
1911-
coordinatorLayoutA11yScrollable =
1912-
addAccessibilityScrollActions(coordinatorLayout, appBarLayout, scrollingView);
19131965
}
19141966

19151967
@Nullable
@@ -1941,74 +1993,6 @@ private boolean childrenHaveScrollFlags(AppBarLayout appBarLayout) {
19411993
return false;
19421994
}
19431995

1944-
private boolean addAccessibilityScrollActions(
1945-
final CoordinatorLayout coordinatorLayout,
1946-
@NonNull final T appBarLayout,
1947-
@NonNull final View scrollingView) {
1948-
boolean a11yScrollable = false;
1949-
if (getTopBottomOffsetForScrollingSibling() != -appBarLayout.getTotalScrollRange()) {
1950-
// Add a collapsing action if the view offset isn't the abl scroll range.
1951-
// (The same offset means the view is completely collapsed). Collapse to minimum height.
1952-
addActionToExpand(coordinatorLayout, appBarLayout, ACTION_SCROLL_FORWARD, false);
1953-
a11yScrollable = true;
1954-
}
1955-
// Don't add an expanding action if the sibling offset is 0, which would mean the abl is
1956-
// completely expanded.
1957-
if (getTopBottomOffsetForScrollingSibling() != 0) {
1958-
if (scrollingView.canScrollVertically(-1)) {
1959-
// Expanding action. If the view can scroll down, expand the app bar reflecting the logic
1960-
// in onNestedPreScroll.
1961-
final int dy = -appBarLayout.getDownNestedPreScrollRange();
1962-
// Offset by non-zero.
1963-
if (dy != 0) {
1964-
ViewCompat.replaceAccessibilityAction(
1965-
coordinatorLayout,
1966-
ACTION_SCROLL_BACKWARD,
1967-
null,
1968-
new AccessibilityViewCommand() {
1969-
@Override
1970-
public boolean perform(@NonNull View view, @Nullable CommandArguments arguments) {
1971-
onNestedPreScroll(
1972-
coordinatorLayout,
1973-
appBarLayout,
1974-
scrollingView,
1975-
0,
1976-
dy,
1977-
new int[] {0, 0},
1978-
ViewCompat.TYPE_NON_TOUCH);
1979-
return true;
1980-
}
1981-
});
1982-
a11yScrollable = true;
1983-
}
1984-
} else {
1985-
// If the view can't scroll down, we are probably at the top of the scrolling content so
1986-
// expand completely.
1987-
addActionToExpand(coordinatorLayout, appBarLayout, ACTION_SCROLL_BACKWARD, true);
1988-
a11yScrollable = true;
1989-
}
1990-
}
1991-
return a11yScrollable;
1992-
}
1993-
1994-
private void addActionToExpand(
1995-
CoordinatorLayout parent,
1996-
@NonNull final T appBarLayout,
1997-
@NonNull AccessibilityActionCompat action,
1998-
final boolean expand) {
1999-
ViewCompat.replaceAccessibilityAction(
2000-
parent,
2001-
action,
2002-
null,
2003-
new AccessibilityViewCommand() {
2004-
@Override
2005-
public boolean perform(@NonNull View view, @Nullable CommandArguments arguments) {
2006-
appBarLayout.setExpanded(expand);
2007-
return true;
2008-
}
2009-
});
2010-
}
2011-
20121996
@Override
20131997
boolean canDragView(T view) {
20141998
if (onDragCallback != null) {
@@ -2112,7 +2096,7 @@ int setHeaderTopBottomOffset(
21122096
offsetDelta = 0;
21132097
}
21142098

2115-
updateAccessibilityActions(coordinatorLayout, appBarLayout);
2099+
addAccessibilityDelegateIfNeeded(coordinatorLayout, appBarLayout);
21162100
return consumed;
21172101
}
21182102

@@ -2410,8 +2394,6 @@ public boolean onDependentViewChanged(
24102394
public void onDependentViewRemoved(
24112395
@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
24122396
if (dependency instanceof AppBarLayout) {
2413-
ViewCompat.removeAccessibilityAction(parent, ACTION_SCROLL_FORWARD.getId());
2414-
ViewCompat.removeAccessibilityAction(parent, ACTION_SCROLL_BACKWARD.getId());
24152397
ViewCompat.setAccessibilityDelegate(parent, null);
24162398
}
24172399
}

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

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,8 @@
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;
28+
import static com.google.common.truth.Truth.assertThat;
2929
import static org.junit.Assert.assertEquals;
30-
import static org.junit.Assert.assertFalse;
31-
import static org.junit.Assert.assertThat;
32-
import static org.junit.Assert.assertTrue;
3330

3431
import android.graphics.Color;
3532
import android.os.Build;
@@ -135,29 +132,31 @@ protected boolean matchesSafely(View view) {
135132

136133
protected void assertAccessibilityHasScrollForwardAction(boolean hasScrollForward) {
137134
if (VERSION.SDK_INT >= 21) {
135+
AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
136+
ViewCompat.onInitializeAccessibilityNodeInfo(mCoordinatorLayout, info);
138137
assertThat(
139-
AccessibilityUtils.hasAction(
140-
mCoordinatorLayout, AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD),
141-
equalTo(hasScrollForward));
138+
AccessibilityUtils.hasAction(
139+
info,
140+
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_FORWARD))
141+
.isEqualTo(hasScrollForward);
142142
}
143143
}
144144

145145
protected void assertAccessibilityHasScrollBackwardAction(boolean hasScrollBackward) {
146146
if (VERSION.SDK_INT >= 21) {
147+
AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
148+
ViewCompat.onInitializeAccessibilityNodeInfo(mCoordinatorLayout, info);
147149
assertThat(
148-
AccessibilityUtils.hasAction(
149-
mCoordinatorLayout, AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD),
150-
equalTo(hasScrollBackward));
150+
AccessibilityUtils.hasAction(
151+
info,
152+
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_BACKWARD))
153+
.isEqualTo(hasScrollBackward);
151154
}
152155
}
153156

154157
protected void assertAccessibilityScrollable(boolean isScrollable) {
155158
AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
156159
ViewCompat.onInitializeAccessibilityNodeInfo(mCoordinatorLayout, info);
157-
if (isScrollable) {
158-
assertTrue(info.isScrollable());
159-
} else {
160-
assertFalse(info.isScrollable());
161-
}
160+
assertThat(info.isScrollable()).isEqualTo(isScrollable);
162161
}
163162
}

0 commit comments

Comments
 (0)