Skip to content

Commit

Permalink
[Carousel] Add a layout listener to recyclerview to refresh keyline s…
Browse files Browse the repository at this point in the history
…tate upon size change

PiperOrigin-RevId: 551280769
  • Loading branch information
imhappi authored and pekingme committed Jul 27, 2023
1 parent 0171624 commit ff52862
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 32 deletions.
Expand Up @@ -40,6 +40,7 @@
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.IntDef;
Expand Down Expand Up @@ -115,14 +116,18 @@ public class CarouselLayoutManager extends LayoutManager

private CarouselOrientationHelper orientationHelper;

/**
* Aligns large items to the start of the carousel.
*/
private final OnLayoutChangeListener recyclerViewSizeChangeListener =
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
// If RV dimen values have changed, refresh the keyline state.
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
v.post(this::refreshKeylineState);
}
};

/** Aligns large items to the start of the carousel. */
public static final int ALIGNMENT_START = 0;

/**
* Aligns large items to the center of the carousel.
*/
/** Aligns large items to the center of the carousel. */
public static final int ALIGNMENT_CENTER = 1;

/**
Expand Down Expand Up @@ -226,9 +231,14 @@ public void setCarouselStrategy(@NonNull CarouselStrategy carouselStrategy) {
@Override
public void onAttachedToWindow(RecyclerView view) {
super.onAttachedToWindow(view);
// TODO: b/291123558 - Keylines should be also be refreshed when
// there's any change affecting dimens of the RecyclerView.
refreshKeylineState();
view.addOnLayoutChangeListener(recyclerViewSizeChangeListener);
}

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

@Override
Expand All @@ -241,16 +251,11 @@ public void onLayoutChildren(Recycler recycler, State state) {

boolean isRtl = isLayoutRtl();

boolean isInitialLoad = keylineStateList == null;
// If a keyline state hasn't been created, use the first child as a representative of how each
// child would like to be measured and allow the strategy to create a keyline state.
boolean isInitialLoad = keylineStateList == null;
if (isInitialLoad) {
View firstChild = recycler.getViewForPosition(0);
measureChildWithMargins(firstChild, 0, 0);
KeylineState keylineState =
carouselStrategy.onFirstChildMeasuredWithMargins(this, firstChild);
keylineStateList =
KeylineStateList.from(this, isRtl ? KeylineState.reverse(keylineState) : keylineState);
recalculateKeylineStateList(recycler);
}

// Ensure our scroll limits are initialized and valid for the data set size.
Expand All @@ -277,12 +282,21 @@ public void onLayoutChildren(Recycler recycler, State state) {
// Ensure currentFillStartPosition is valid if the number of items in the adapter has changed.
currentFillStartPosition = MathUtils.clamp(currentFillStartPosition, 0, state.getItemCount());

updateCurrentKeylineStateForScrollOffset();
updateCurrentKeylineStateForScrollOffset(keylineStateList);

detachAndScrapAttachedViews(recycler);
fill(recycler, state);
}

private void recalculateKeylineStateList(Recycler recycler) {
View firstChild = recycler.getViewForPosition(0);
measureChildWithMargins(firstChild, 0, 0);
KeylineState keylineState = carouselStrategy.onFirstChildMeasuredWithMargins(this, firstChild);
keylineStateList =
KeylineStateList.from(
this, isLayoutRtl() ? KeylineState.reverse(keylineState) : keylineState);
}

/**
* Recalculates the {@link KeylineState} and {@link KeylineStateList} for the current strategy.
*/
Expand Down Expand Up @@ -683,7 +697,8 @@ private static KeylineRange getSurroundingKeylineRange(
*
* <p>This method should be called any time a change in the scroll offset occurs.
*/
private void updateCurrentKeylineStateForScrollOffset() {
private void updateCurrentKeylineStateForScrollOffset(
@NonNull KeylineStateList keylineStateList) {
if (maxScroll <= minScroll) {
// We don't have enough items in the list to scroll and we should use the keyline state
// that aligns items to the start of the container.
Expand All @@ -697,9 +712,9 @@ private void updateCurrentKeylineStateForScrollOffset() {
}

/**
* Calculates the horizontal distance children should be scrolled by for a given {@code dx}.
* Calculates the distance children should be scrolled by for a given {@code delta}.
*
* @param dx the delta, resulting from a gesture or other event, that the list would like to be
* @param delta the delta, resulting from a gesture or other event, that the list would like to be
* scrolled by
* @param currentScroll the current horizontal scroll offset that is always between the min and
* max horizontal scroll
Expand All @@ -724,7 +739,7 @@ private static int calculateShouldScrollBy(
* Calculates the total offset needed to scroll the first item in the list to the center of the
* start focal keyline.
*/
private int calculateStartScroll(KeylineStateList stateList) {
private int calculateStartScroll(@NonNull KeylineStateList stateList) {
boolean isRtl = isLayoutRtl();
KeylineState startState = isRtl ? stateList.getEndState() : stateList.getStartState();
Keyline startFocalKeyline =
Expand Down Expand Up @@ -1106,7 +1121,7 @@ public void scrollToPosition(int position) {
}
scrollOffset = getScrollOffsetForPosition(position, getKeylineStateForPosition(position));
currentFillStartPosition = MathUtils.clamp(position, 0, max(0, getItemCount() - 1));
updateCurrentKeylineStateForScrollOffset();
updateCurrentKeylineStateForScrollOffset(keylineStateList);
requestLayout();
}

Expand Down Expand Up @@ -1211,10 +1226,14 @@ private int scrollBy(int distance, Recycler recycler, State state) {
return 0;
}

if (keylineStateList == null) {
recalculateKeylineStateList(recycler);
}

// Calculate how much the carousel should scroll and update the scroll offset.
int scrolledBy = calculateShouldScrollBy(distance, scrollOffset, minScroll, maxScroll);
scrollOffset += scrolledBy;
updateCurrentKeylineStateForScrollOffset();
updateCurrentKeylineStateForScrollOffset(keylineStateList);

float halfItemSize = currentKeylineState.getItemSize() / 2F;
int startPosition = getPosition(getChildAt(0));
Expand Down Expand Up @@ -1278,7 +1297,7 @@ public int computeHorizontalScrollOffset(@NonNull State state) {
*/
@Override
public int computeHorizontalScrollExtent(@NonNull State state) {
return (int) keylineStateList.getDefaultState().getItemSize();
return keylineStateList == null ? 0 : (int) keylineStateList.getDefaultState().getItemSize();
}

/**
Expand All @@ -1300,7 +1319,7 @@ public int computeVerticalScrollOffset(@NonNull State state) {

@Override
public int computeVerticalScrollExtent(@NonNull State state) {
return (int) keylineStateList.getDefaultState().getItemSize();
return keylineStateList == null ? 0 : (int) keylineStateList.getDefaultState().getItemSize();
}

@Override
Expand Down
Expand Up @@ -43,6 +43,7 @@
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

/** RTL tests for {@link CarouselLayoutManager}. */
@RunWith(RobolectricTestRunner.class)
Expand Down Expand Up @@ -87,17 +88,10 @@ public void testFirstAdapterItem_isDrawnAtRightOfContainer() throws Throwable {
assertThat(firstChild.getRight()).isEqualTo(DEFAULT_RECYCLER_VIEW_WIDTH);
}

@Config(qualifiers = "sw1320dp-w1320dp")
@Test
public void testScrollBeyondMaxHorizontalScroll_shouldLimitToMaxScrollOffset() throws Throwable {
KeylineState keylineState = getTestCenteredKeylineState();
layoutManager.setCarouselStrategy(
new CarouselStrategy() {
@Override
KeylineState onFirstChildMeasuredWithMargins(
@NonNull Carousel carousel, @NonNull View child) {
return keylineState;
}
});
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
scrollToPosition(recyclerView, layoutManager, 200);

Expand Down

0 comments on commit ff52862

Please sign in to comment.