|
19 | 19 | import com.google.android.material.R; |
20 | 20 |
|
21 | 21 | 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; |
22 | 24 |
|
23 | 25 | import android.animation.ValueAnimator; |
24 | 26 | import android.animation.ValueAnimator.AnimatorUpdateListener; |
|
50 | 52 | import androidx.core.view.ViewCompat; |
51 | 53 | import androidx.core.view.ViewCompat.NestedScrollType; |
52 | 54 | import androidx.core.view.WindowInsetsCompat; |
| 55 | +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; |
| 56 | +import androidx.core.view.accessibility.AccessibilityViewCommand; |
53 | 57 | import androidx.appcompat.content.res.AppCompatResources; |
54 | 58 | import android.util.AttributeSet; |
55 | 59 | import android.view.View; |
@@ -1020,7 +1024,6 @@ public static class LayoutParams extends LinearLayout.LayoutParams { |
1020 | 1024 | }) |
1021 | 1025 | @Retention(RetentionPolicy.SOURCE) |
1022 | 1026 | public @interface ScrollFlags {} |
1023 | | - |
1024 | 1027 | /** |
1025 | 1028 | * Disable scrolling on the view. This flag should not be combined with any of the other scroll |
1026 | 1029 | * flags. |
@@ -1324,6 +1327,12 @@ public void onNestedScroll( |
1324 | 1327 | consumed[1] = |
1325 | 1328 | scroll(coordinatorLayout, child, dyUnconsumed, -child.getDownNestedScrollRange(), 0); |
1326 | 1329 | } |
| 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 | + } |
1327 | 1336 | } |
1328 | 1337 |
|
1329 | 1338 | @Override |
@@ -1560,9 +1569,94 @@ public boolean onLayoutChild( |
1560 | 1569 | // Make sure we dispatch the offset update |
1561 | 1570 | abl.onOffsetChanged(getTopAndBottomOffset()); |
1562 | 1571 |
|
| 1572 | + updateAccessibilityActions(parent, abl); |
1563 | 1573 | return handled; |
1564 | 1574 | } |
1565 | 1575 |
|
| 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 | + |
1566 | 1660 | @Override |
1567 | 1661 | boolean canDragView(T view) { |
1568 | 1662 | if (onDragCallback != null) { |
@@ -1653,6 +1747,7 @@ int setHeaderTopBottomOffset( |
1653 | 1747 | offsetDelta = 0; |
1654 | 1748 | } |
1655 | 1749 |
|
| 1750 | + updateAccessibilityActions(coordinatorLayout, appBarLayout); |
1656 | 1751 | return consumed; |
1657 | 1752 | } |
1658 | 1753 |
|
@@ -1922,6 +2017,15 @@ public boolean onDependentViewChanged( |
1922 | 2017 | return false; |
1923 | 2018 | } |
1924 | 2019 |
|
| 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 | + |
1925 | 2029 | @Override |
1926 | 2030 | public boolean onRequestChildRectangleOnScreen( |
1927 | 2031 | @NonNull CoordinatorLayout parent, |
|
0 commit comments