Skip to content

Commit

Permalink
[Carousel] Add a11y focus scrolling support
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 505783943
  • Loading branch information
hunterstich authored and dsn5ft committed Jan 31, 2023
1 parent d0d0f54 commit 0f179b3
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.material.carousel;

import android.graphics.Rect;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;

/** A class that handles accessibility for CarouselLayoutManager. */
final class CarouselAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {

public CarouselAccessibilityDelegate(@NonNull RecyclerView recyclerView) {
super(recyclerView);
}

@Override
public boolean onRequestSendAccessibilityEvent(
@NonNull ViewGroup host, @NonNull View child, @NonNull AccessibilityEvent event) {
switch (event.getEventType()) {
// Allow every child in the carousel an opportunity to bring itself into the focal range
// when focused by accessibility.
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
RecyclerView rv = (RecyclerView) host;
Rect rect = new Rect(0, 0, child.getWidth(), child.getHeight());
rv.getLayoutManager()
.requestChildRectangleOnScreen(
(RecyclerView) host,
child,
rect,
/* immediate= */ true,
/* focusedChildVisible= */ false);
return true;
default:
return super.onRequestSendAccessibilityEvent(host, child, event);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
Expand Down Expand Up @@ -120,6 +121,18 @@ public CarouselLayoutManager(
// TODO(b/238620200): Add and obtain carousel attrs set on RecyclerView
}

@Override
public void onAttachedToWindow(RecyclerView view) {
super.onAttachedToWindow(view);
view.setAccessibilityDelegateCompat(new CarouselAccessibilityDelegate(view));
}

@Override
public void onDetachedFromWindow(RecyclerView view, Recycler recycler) {
super.onDetachedFromWindow(view, recycler);
view.setAccessibilityDelegateCompat(null);
}

@Override
public LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(
Expand Down Expand Up @@ -724,6 +737,15 @@ private int addEnd(int value, int amount) {
return isLayoutRtl() ? value - amount : value + amount;
}

@Override
public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
if (getChildCount() > 0) {
event.setFromIndex(getPosition(getChildAt(0)));
event.setToIndex(getPosition(getChildAt(getChildCount() - 1)));
}
}

/**
* Gets the scroll offset for a position in the adapter.
*
Expand Down Expand Up @@ -768,6 +790,30 @@ public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
return canScrollHorizontally() ? scrollBy(dx, recycler, state) : 0;
}

@Override
public boolean requestChildRectangleOnScreen(
@NonNull RecyclerView parent,
@NonNull View child,
@NonNull Rect rect,
boolean immediate,
boolean focusedChildVisible) {
if (keylineStateList == null) {
return false;
}

int offsetForChild =
getScrollOffsetForPosition(keylineStateList.getDefaultState(), getPosition(child));
int dx = offsetForChild - horizontalScrollOffset;
if (!focusedChildVisible) {
if (dx != 0) {
// TODO(b/266816148): Implement smoothScrollBy when immediate is false.
parent.scrollBy(dx, 0);
return true;
}
}
return false;
}

/**
* Offset child items, respecting min and max scroll offsets, and fill additional space with new
* items.
Expand Down Expand Up @@ -832,6 +878,45 @@ private void offsetChildLeftAndRight(
child.offsetLeftAndRight((int) (offsetCx - actualCx));
}

/**
* Calculate the offset of the horizontal scrollbar thumb within the horizontal range. This is the
* position of the thumb within the scrollbar track.
*
* <p>This is also used for accessibility when scrolling to give auditory feedback about the
* current scroll position within the total range.
*
* <p>This method can return an arbitrary unit as long as the unit is shared across {@link
* #computeHorizontalScrollExtent(State)} and {@link #computeHorizontalScrollRange(State)}.
*/
@Override
public int computeHorizontalScrollOffset(@NonNull State state) {
return horizontalScrollOffset;
}

/**
* Compute the extent of the horizontal scrollbar thumb. This is the size of the thumb inside the
* scrollbar track.
*
* <p>This method can return an arbitrary unit as long as the unit is shared across {@link
* #computeHorizontalScrollExtent(State)} and {@link #computeHorizontalScrollOffset(State)}.
*/
@Override
public int computeHorizontalScrollExtent(@NonNull State state) {
return (int) keylineStateList.getDefaultState().getItemSize();
}

/**
* Compute the horizontal range represented by the horizontal scroll bars. This is the total
* length of the scrollbar track within the range.
*
* <p>This method can return an arbitrary unit as long as the unit is shared across {@link
* #computeHorizontalScrollExtent(State)} and {@link #computeHorizontalScrollOffset(State)}.
*/
@Override
public int computeHorizontalScrollRange(@NonNull State state) {
return maxHorizontalScroll - minHorizontalScroll;
}

/**
* Enables drawing that illustrates keylines and other internal concepts to help debug
* configurations.
Expand Down

0 comments on commit 0f179b3

Please sign in to comment.