Skip to content

Commit

Permalink
[Carousel] Update scroll offset to scroll to the estimated position t…
Browse files Browse the repository at this point in the history
…hat it was at upon an initial load

Resolves #3590

PiperOrigin-RevId: 568642330
  • Loading branch information
imhappi authored and afohrman committed Sep 27, 2023
1 parent c418063 commit 4a6ae4d
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 6 deletions.
Expand Up @@ -18,6 +18,7 @@

import com.google.android.material.R;

import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static com.google.android.material.animation.AnimationUtils.lerp;
import static java.lang.Math.abs;
import static java.lang.Math.max;
Expand Down Expand Up @@ -130,6 +131,13 @@ public class CarouselLayoutManager extends LayoutManager
/** Aligns large items to the center of the carousel. */
public static final int ALIGNMENT_CENTER = 1;

/**
* An estimation of the current focused position, determined by which item center is closest to
* the first focal keyline. This is used when restoring item position after the carousel keylines
* are re-calculated due to configuration or size changes.
*/
private int currentEstimatedPosition = NO_POSITION;

/**
* Determines where to align the large items in the carousel.
*
Expand Down Expand Up @@ -261,6 +269,7 @@ public void onLayoutChildren(Recycler recycler, State state) {
// Ensure our scroll limits are initialized and valid for the data set size.
int startScroll = calculateStartScroll(keylineStateList);
int endScroll = calculateEndScroll(state, keylineStateList);

// Convert the layout-direction-aware offsets into min/max absolutes. These need to be in the
// min/max format so they can be correctly passed to KeylineStateList and used to interpolate
// between keyline states.
Expand All @@ -273,12 +282,17 @@ public void onLayoutChildren(Recycler recycler, State state) {
keylineStatePositionMap =
keylineStateList.getKeylineStateForPositionMap(
getItemCount(), minScroll, maxScroll, isLayoutRtl());
} else {
// Clamp the horizontal scroll offset by the new min and max by pinging the scroll by
// calculator with a 0 delta.
scrollOffset += calculateShouldScrollBy(0, scrollOffset, minScroll, maxScroll);
if (currentEstimatedPosition != NO_POSITION) {
scrollOffset =
getScrollOffsetForPosition(
currentEstimatedPosition, getKeylineStateForPosition(currentEstimatedPosition));
}
}

// Clamp the horizontal scroll offset by the new min and max by pinging the scroll by
// calculator with a 0 delta.
scrollOffset += calculateShouldScrollBy(0, scrollOffset, minScroll, maxScroll);

// Ensure currentFillStartPosition is valid if the number of items in the adapter has changed.
currentFillStartPosition = MathUtils.clamp(currentFillStartPosition, 0, state.getItemCount());

Expand Down Expand Up @@ -1124,6 +1138,7 @@ private KeylineState getKeylineStateForPosition(int position) {

@Override
public void scrollToPosition(int position) {
currentEstimatedPosition = position;
if (keylineStateList == null) {
return;
}
Expand Down Expand Up @@ -1251,9 +1266,19 @@ private int scrollBy(int distance, Recycler recycler, State state) {
int startPosition = getPosition(getChildAt(0));
float start = calculateChildStartForFill(startPosition);
Rect boundsRect = new Rect();
float firstFocalKeylineLoc =
isLayoutRtl()
? currentKeylineState.getLastFocalKeyline().locOffset
: currentKeylineState.getFirstFocalKeyline().locOffset;
float absDistanceToFirstFocal = Float.MAX_VALUE;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
offsetChild(child, start, halfItemSize, boundsRect);
float offsetCenter = offsetChild(child, start, halfItemSize, boundsRect);
float distanceToFirstFocal = Math.abs(firstFocalKeylineLoc - offsetCenter);
if (child != null && distanceToFirstFocal < absDistanceToFirstFocal) {
absDistanceToFirstFocal = distanceToFirstFocal;
currentEstimatedPosition = getPosition(child);
}
start = addEnd(start, currentKeylineState.getItemSize());
}

Expand All @@ -1272,8 +1297,9 @@ private int scrollBy(int distance, Recycler recycler, State state) {
* after the child has been offset
* @param halfItemSize half of the fully unmasked item size
* @param boundsRect a Rect to use to find the current bounds of {@code child}
* @return the center in which the child is offset to
*/
private void offsetChild(View child, float startOffset, float halfItemSize, Rect boundsRect) {
private float offsetChild(View child, float startOffset, float halfItemSize, Rect boundsRect) {
float center = addEnd(startOffset, halfItemSize);
KeylineRange range =
getSurroundingKeylineRange(currentKeylineState.getKeylines(), center, false);
Expand All @@ -1283,6 +1309,7 @@ private void offsetChild(View child, float startOffset, float halfItemSize, Rect
super.getDecoratedBoundsWithMargins(child, boundsRect);
updateChildMaskForLocation(child, center, range);
orientationHelper.offsetChild(child, boundsRect, halfItemSize, offsetCenter);
return offsetCenter;
}

/**
Expand Down
Expand Up @@ -578,6 +578,19 @@ public void testMaskOnBottomParentEdge_areRoundedUp() throws Throwable {
assertThat(secondChildMask.top).isAtMost(secondChildMask.bottom);
}

@Test
public void testScrollOffset_isNotReset() throws Throwable {
int itemCount = 10;
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(itemCount));
assertThat(layoutManager.scrollOffset).isEqualTo(layoutManager.minScroll);

scrollToPosition(recyclerView, layoutManager, itemCount / 2);

setVerticalOrientation(recyclerView, layoutManager);
assertThat(layoutManager.scrollOffset).isNotEqualTo(layoutManager.minScroll);
assertThat(layoutManager.computeScrollVectorForPosition(itemCount / 2).y).isEqualTo(0f);
}

/**
* Assigns explicit sizes to fixtures being used to construct the testing environment.
*
Expand Down

0 comments on commit 4a6ae4d

Please sign in to comment.