Skip to content

Commit

Permalink
[Carousel] Fixed keyline shifting in RTL for uncontained carousels
Browse files Browse the repository at this point in the history
Resolves #3554
Resolves #3580

PiperOrigin-RevId: 565654556
  • Loading branch information
hunterstich authored and leticiarossi committed Sep 15, 2023
1 parent 4ce7e4c commit 7151714
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 58 deletions.
10 changes: 6 additions & 4 deletions lib/java/com/google/android/material/carousel/KeylineState.java
Expand Up @@ -186,16 +186,19 @@ static KeylineState reverse(KeylineState keylineState, float availableSpace) {
KeylineState.Builder builder =
new KeylineState.Builder(keylineState.getItemSize(), availableSpace);

// The new start offset should now be the same distance from the left of the carousel container
// as the last item's right was from the right of the container.
float start =
keylineState.getFirstKeyline().locOffset
- (keylineState.getFirstKeyline().maskedItemSize / 2F);
availableSpace
- keylineState.getLastKeyline().locOffset
- (keylineState.getLastKeyline().maskedItemSize / 2F);
for (int i = keylineState.getKeylines().size() - 1; i >= 0; i--) {
Keyline k = keylineState.getKeylines().get(i);
float offset = start + (k.maskedItemSize / 2F);
boolean isFocal =
i >= keylineState.getFirstFocalKeylineIndex()
&& i <= keylineState.getLastFocalKeylineIndex();
builder.addKeyline(offset, k.mask, k.maskedItemSize, isFocal);
builder.addKeyline(offset, k.mask, k.maskedItemSize, isFocal, k.isAnchor);
start += k.maskedItemSize;
}

Expand Down Expand Up @@ -243,7 +246,6 @@ static final class Builder {

private int latestAnchorKeylineIndex = NO_INDEX;


/**
* Creates a new {@link KeylineState.Builder}.
*
Expand Down
120 changes: 72 additions & 48 deletions lib/java/com/google/android/material/carousel/KeylineStateList.java
Expand Up @@ -315,7 +315,29 @@ private static float[] getStateStepInterpolationPoints(
private static boolean isFirstFocalItemAtLeftOfContainer(KeylineState state) {
float firstFocalItemLeft =
state.getFirstFocalKeyline().locOffset - (state.getFirstFocalKeyline().maskedItemSize / 2F);
return firstFocalItemLeft <= 0F || state.getFirstFocalKeyline() == state.getFirstKeyline();
return firstFocalItemLeft >= 0F
&& state.getFirstFocalKeyline() == state.getFirstNonAnchorKeyline();
}

/**
* Determines whether or not the first focal item for the given {@code state} is at the right of
* the carousel container and fully visible.
*
* @param carousel the {@link Carousel} associated with this {@link KeylineStateList}.
* @param state the state to check for right item position
* @return true if the {@code state}'s first focal item has its right aligned with the right of
* the {@code carousel} container and is fully visible.
*/
private static boolean isLastFocalItemVisibleAtRightOfContainer(
Carousel carousel, KeylineState state) {
int containerSize = carousel.getContainerHeight();
if (carousel.isHorizontal()) {
containerSize = carousel.getContainerWidth();
}
float lastFocalItemRight =
state.getLastFocalKeyline().locOffset + (state.getLastFocalKeyline().maskedItemSize / 2F);
return lastFocalItemRight <= containerSize
&& state.getLastFocalKeyline() == state.getLastNonAnchorKeyline();
}

/**
Expand All @@ -339,6 +361,7 @@ private static List<KeylineState> getStateStepsStart(
List<KeylineState> steps = new ArrayList<>();
steps.add(defaultState);
int firstNonAnchorKeylineIndex = findFirstNonAnchorKeylineIndex(defaultState);

// If the first focal item is already at the left of the container or there are no in bounds
// keylines, return a list of steps that only includes the default state (there is nowhere to
// shift).
Expand All @@ -348,15 +371,26 @@ private static List<KeylineState> getStateStepsStart(
}

int start = firstNonAnchorKeylineIndex;
int end = defaultState.getFirstFocalKeylineIndex() - 1;
int end = defaultState.getFirstFocalKeylineIndex();
int numberOfSteps = end - start;
float cutoffs = 0;

float carouselSize =
carousel.isHorizontal() ? carousel.getContainerWidth() : carousel.getContainerHeight();
float originalStart =
defaultState.getFirstKeyline().locOffset
- (defaultState.getFirstKeyline().maskedItemSize / 2F);

for (int i = 0; i <= numberOfSteps; i++) {
if (numberOfSteps <= 0 && defaultState.getFirstFocalKeyline().cutoff > 0) {
// If there are no steps, there still might be a cutoff focal item that we should shift into
// view. Add a step that shifts all the keylines over to bring the first focal item into full
// view.
float cutoffs = defaultState.getFirstFocalKeyline().cutoff;
steps.add(
shiftKeylinesAndCreateKeylineState(defaultState, originalStart + cutoffs, carouselSize));
return steps;
}

float cutoffs = 0;
for (int i = 0; i < numberOfSteps; i++) {
KeylineState prevStepState = steps.get(steps.size() - 1);
int itemOrigIndex = start + i;
// If this is the first item from the original state, place it at the end of the dest state.
Expand All @@ -382,35 +416,12 @@ private static List<KeylineState> getStateStepsStart(
originalStart + cutoffs,
newFirstFocalIndex,
newLastFocalIndex,
carousel.isHorizontal()
? carousel.getContainerWidth()
: carousel.getContainerHeight());
carouselSize);
steps.add(shifted);
}
return steps;
}

/**
* Determines whether or not the first focal item for the given {@code state} is at the right of
* the carousel container and fully visible.
*
* @param carousel the {@link Carousel} associated with this {@link KeylineStateList}.
* @param state the state to check for right item position
* @return true if the {@code state}'s first focal item has its right aligned with the right of
* the {@code carousel} container and is fully visible.
*/
private static boolean isLastFocalItemVisibleAtRightOfContainer(
Carousel carousel, KeylineState state) {
int containerSize = carousel.getContainerHeight();
if (carousel.isHorizontal()) {
containerSize = carousel.getContainerWidth();
}
float lastFocalItemRight =
state.getLastFocalKeyline().locOffset + (state.getLastFocalKeyline().maskedItemSize / 2F);
return lastFocalItemRight <= containerSize
&& state.getLastFocalKeyline() == state.getLastNonAnchorKeyline();
}

/**
* Generates discreet steps which move the focal range from it's original position until it
* reaches the right of the carousel container.
Expand Down Expand Up @@ -441,35 +452,26 @@ private static List<KeylineState> getStateStepsEnd(
return steps;
}

int start = defaultState.getLastFocalKeylineIndex();
int end = lastNonAnchorKeylineIndex;
int numberOfSteps = end - start;
float carouselSize =
carousel.isHorizontal() ? carousel.getContainerWidth() : carousel.getContainerHeight();

float originalStart =
defaultState.getFirstKeyline().locOffset
- (defaultState.getFirstKeyline().maskedItemSize / 2F);

// If we are here, it means that the last keyline is focal, but is cut off
// since it is not visible. If this is the case, we want to add one step where
// the keylines are shifted so that the last keyline is visible.
if (defaultState.getLastFocalKeyline() == defaultState.getLastNonAnchorKeyline()) {
KeylineState shifted =
moveKeylineAndCreateKeylineState(
defaultState,
/* keylineSrcIndex= */ defaultState.getLastFocalKeylineIndex(),
/* keylineDstIndex= */ defaultState.getFirstFocalKeylineIndex(),
originalStart - defaultState.getLastFocalKeyline().cutoff,
defaultState.getFirstFocalKeylineIndex(),
defaultState.getLastFocalKeylineIndex(),
carouselSize);
steps.add(shifted);
if (numberOfSteps <= 0 && defaultState.getLastFocalKeyline().cutoff > 0) {
// If there are no steps, there still might be a cutoff focal item that we should shift into
// view. Add a step that shifts all the keylines over to bring the last focal item into full
// view.
float cutoffs = defaultState.getLastFocalKeyline().cutoff;
steps.add(
shiftKeylinesAndCreateKeylineState(defaultState, originalStart - cutoffs, carouselSize));
return steps;
}

int start = defaultState.getLastFocalKeylineIndex();
int end = lastNonAnchorKeylineIndex;
int numberOfSteps = end - start;
float cutoffs = 0;

for (int i = 0; i < numberOfSteps; i++) {
KeylineState prevStepState = steps.get(steps.size() - 1);
int itemOrigIndex = end - i;
Expand Down Expand Up @@ -503,6 +505,28 @@ private static List<KeylineState> getStateStepsEnd(
return steps;
}

/**
* Creates a new, valid KeylineState that has the same order as {@code state} but with all
* keylines shifted along the scrolling axis.
*
* @param state the state to shift
* @param startOffset the point along the scrolling axis where keylines should start being added
* from
* @param carouselSize the size of the carousel container
* @return a new {@link KeylineState} with the shifted keylines
*/
private static KeylineState shiftKeylinesAndCreateKeylineState(
KeylineState state, float startOffset, float carouselSize) {
return moveKeylineAndCreateKeylineState(
state,
0,
0,
startOffset,
state.getFirstFocalKeylineIndex(),
state.getLastFocalKeylineIndex(),
carouselSize);
}

/**
* Creates a new, valid KeylineState from a list of keylines that have been re-arranged.
*
Expand Down
Expand Up @@ -113,18 +113,22 @@ public void testReverseKeylines_shouldReverse() {
// Extra small items are 10F, Small items are 50F, large items are 100F
KeylineState keylineState =
new KeylineState.Builder(100F, recyclerWidth)
.addKeyline(-5F, getKeylineMaskPercentage(10F, 100F), 10F)
// left edge of xSmall item is -10 from left edge of carousel container
.addKeyline(-5F, getKeylineMaskPercentage(10F, 100F), 10F, false, true)
.addKeyline(50F, 0F, 100F, true)
.addKeyline(125F, getKeylineMaskPercentage(50F, 100F), 50F)
.addKeyline(155F, getKeylineMaskPercentage(10F, 100F), 10F)
// right edge of xSmall item is 60 from right edge of carousel container
.addKeyline(155F, getKeylineMaskPercentage(10F, 100F), 10F, false, true)
.build();

KeylineState expectedState =
new KeylineState.Builder(100F, recyclerWidth)
.addKeyline(-5F, getKeylineMaskPercentage(10F, 100F), 10F)
.addKeyline(25F, getKeylineMaskPercentage(50, 100F), 50F)
.addKeyline(100F, 0F, 100F, true)
.addKeyline(155F, getKeylineMaskPercentage(10F, 100F), 10F)
// left edge of xSmall item is -60 from left of carousel container
.addKeyline(-55F, getKeylineMaskPercentage(10F, 100F), 10F, false, true)
.addKeyline(-25F, getKeylineMaskPercentage(50, 100F), 50F)
.addKeyline(50F, 0F, 100F, true)
// right edge of xSmall item is 10 from right of carousel container
.addKeyline(105F, getKeylineMaskPercentage(10F, 100F), 10F, false, true)
.build();
KeylineState reversedState = KeylineState.reverse(keylineState, recyclerWidth);

Expand Down

0 comments on commit 7151714

Please sign in to comment.