Skip to content

Commit 7151714

Browse files
hunterstichleticiarossi
authored andcommittedSep 15, 2023
[Carousel] Fixed keyline shifting in RTL for uncontained carousels
Resolves #3554 Resolves #3580 PiperOrigin-RevId: 565654556
1 parent 4ce7e4c commit 7151714

File tree

3 files changed

+88
-58
lines changed

3 files changed

+88
-58
lines changed
 

‎lib/java/com/google/android/material/carousel/KeylineState.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -186,16 +186,19 @@ static KeylineState reverse(KeylineState keylineState, float availableSpace) {
186186
KeylineState.Builder builder =
187187
new KeylineState.Builder(keylineState.getItemSize(), availableSpace);
188188

189+
// The new start offset should now be the same distance from the left of the carousel container
190+
// as the last item's right was from the right of the container.
189191
float start =
190-
keylineState.getFirstKeyline().locOffset
191-
- (keylineState.getFirstKeyline().maskedItemSize / 2F);
192+
availableSpace
193+
- keylineState.getLastKeyline().locOffset
194+
- (keylineState.getLastKeyline().maskedItemSize / 2F);
192195
for (int i = keylineState.getKeylines().size() - 1; i >= 0; i--) {
193196
Keyline k = keylineState.getKeylines().get(i);
194197
float offset = start + (k.maskedItemSize / 2F);
195198
boolean isFocal =
196199
i >= keylineState.getFirstFocalKeylineIndex()
197200
&& i <= keylineState.getLastFocalKeylineIndex();
198-
builder.addKeyline(offset, k.mask, k.maskedItemSize, isFocal);
201+
builder.addKeyline(offset, k.mask, k.maskedItemSize, isFocal, k.isAnchor);
199202
start += k.maskedItemSize;
200203
}
201204

@@ -243,7 +246,6 @@ static final class Builder {
243246

244247
private int latestAnchorKeylineIndex = NO_INDEX;
245248

246-
247249
/**
248250
* Creates a new {@link KeylineState.Builder}.
249251
*

‎lib/java/com/google/android/material/carousel/KeylineStateList.java

+72-48
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,29 @@ private static float[] getStateStepInterpolationPoints(
315315
private static boolean isFirstFocalItemAtLeftOfContainer(KeylineState state) {
316316
float firstFocalItemLeft =
317317
state.getFirstFocalKeyline().locOffset - (state.getFirstFocalKeyline().maskedItemSize / 2F);
318-
return firstFocalItemLeft <= 0F || state.getFirstFocalKeyline() == state.getFirstKeyline();
318+
return firstFocalItemLeft >= 0F
319+
&& state.getFirstFocalKeyline() == state.getFirstNonAnchorKeyline();
320+
}
321+
322+
/**
323+
* Determines whether or not the first focal item for the given {@code state} is at the right of
324+
* the carousel container and fully visible.
325+
*
326+
* @param carousel the {@link Carousel} associated with this {@link KeylineStateList}.
327+
* @param state the state to check for right item position
328+
* @return true if the {@code state}'s first focal item has its right aligned with the right of
329+
* the {@code carousel} container and is fully visible.
330+
*/
331+
private static boolean isLastFocalItemVisibleAtRightOfContainer(
332+
Carousel carousel, KeylineState state) {
333+
int containerSize = carousel.getContainerHeight();
334+
if (carousel.isHorizontal()) {
335+
containerSize = carousel.getContainerWidth();
336+
}
337+
float lastFocalItemRight =
338+
state.getLastFocalKeyline().locOffset + (state.getLastFocalKeyline().maskedItemSize / 2F);
339+
return lastFocalItemRight <= containerSize
340+
&& state.getLastFocalKeyline() == state.getLastNonAnchorKeyline();
319341
}
320342

321343
/**
@@ -339,6 +361,7 @@ private static List<KeylineState> getStateStepsStart(
339361
List<KeylineState> steps = new ArrayList<>();
340362
steps.add(defaultState);
341363
int firstNonAnchorKeylineIndex = findFirstNonAnchorKeylineIndex(defaultState);
364+
342365
// If the first focal item is already at the left of the container or there are no in bounds
343366
// keylines, return a list of steps that only includes the default state (there is nowhere to
344367
// shift).
@@ -348,15 +371,26 @@ private static List<KeylineState> getStateStepsStart(
348371
}
349372

350373
int start = firstNonAnchorKeylineIndex;
351-
int end = defaultState.getFirstFocalKeylineIndex() - 1;
374+
int end = defaultState.getFirstFocalKeylineIndex();
352375
int numberOfSteps = end - start;
353-
float cutoffs = 0;
354-
376+
float carouselSize =
377+
carousel.isHorizontal() ? carousel.getContainerWidth() : carousel.getContainerHeight();
355378
float originalStart =
356379
defaultState.getFirstKeyline().locOffset
357380
- (defaultState.getFirstKeyline().maskedItemSize / 2F);
358381

359-
for (int i = 0; i <= numberOfSteps; i++) {
382+
if (numberOfSteps <= 0 && defaultState.getFirstFocalKeyline().cutoff > 0) {
383+
// If there are no steps, there still might be a cutoff focal item that we should shift into
384+
// view. Add a step that shifts all the keylines over to bring the first focal item into full
385+
// view.
386+
float cutoffs = defaultState.getFirstFocalKeyline().cutoff;
387+
steps.add(
388+
shiftKeylinesAndCreateKeylineState(defaultState, originalStart + cutoffs, carouselSize));
389+
return steps;
390+
}
391+
392+
float cutoffs = 0;
393+
for (int i = 0; i < numberOfSteps; i++) {
360394
KeylineState prevStepState = steps.get(steps.size() - 1);
361395
int itemOrigIndex = start + i;
362396
// If this is the first item from the original state, place it at the end of the dest state.
@@ -382,35 +416,12 @@ private static List<KeylineState> getStateStepsStart(
382416
originalStart + cutoffs,
383417
newFirstFocalIndex,
384418
newLastFocalIndex,
385-
carousel.isHorizontal()
386-
? carousel.getContainerWidth()
387-
: carousel.getContainerHeight());
419+
carouselSize);
388420
steps.add(shifted);
389421
}
390422
return steps;
391423
}
392424

393-
/**
394-
* Determines whether or not the first focal item for the given {@code state} is at the right of
395-
* the carousel container and fully visible.
396-
*
397-
* @param carousel the {@link Carousel} associated with this {@link KeylineStateList}.
398-
* @param state the state to check for right item position
399-
* @return true if the {@code state}'s first focal item has its right aligned with the right of
400-
* the {@code carousel} container and is fully visible.
401-
*/
402-
private static boolean isLastFocalItemVisibleAtRightOfContainer(
403-
Carousel carousel, KeylineState state) {
404-
int containerSize = carousel.getContainerHeight();
405-
if (carousel.isHorizontal()) {
406-
containerSize = carousel.getContainerWidth();
407-
}
408-
float lastFocalItemRight =
409-
state.getLastFocalKeyline().locOffset + (state.getLastFocalKeyline().maskedItemSize / 2F);
410-
return lastFocalItemRight <= containerSize
411-
&& state.getLastFocalKeyline() == state.getLastNonAnchorKeyline();
412-
}
413-
414425
/**
415426
* Generates discreet steps which move the focal range from it's original position until it
416427
* reaches the right of the carousel container.
@@ -441,35 +452,26 @@ private static List<KeylineState> getStateStepsEnd(
441452
return steps;
442453
}
443454

455+
int start = defaultState.getLastFocalKeylineIndex();
456+
int end = lastNonAnchorKeylineIndex;
457+
int numberOfSteps = end - start;
444458
float carouselSize =
445459
carousel.isHorizontal() ? carousel.getContainerWidth() : carousel.getContainerHeight();
446-
447460
float originalStart =
448461
defaultState.getFirstKeyline().locOffset
449462
- (defaultState.getFirstKeyline().maskedItemSize / 2F);
450463

451-
// If we are here, it means that the last keyline is focal, but is cut off
452-
// since it is not visible. If this is the case, we want to add one step where
453-
// the keylines are shifted so that the last keyline is visible.
454-
if (defaultState.getLastFocalKeyline() == defaultState.getLastNonAnchorKeyline()) {
455-
KeylineState shifted =
456-
moveKeylineAndCreateKeylineState(
457-
defaultState,
458-
/* keylineSrcIndex= */ defaultState.getLastFocalKeylineIndex(),
459-
/* keylineDstIndex= */ defaultState.getFirstFocalKeylineIndex(),
460-
originalStart - defaultState.getLastFocalKeyline().cutoff,
461-
defaultState.getFirstFocalKeylineIndex(),
462-
defaultState.getLastFocalKeylineIndex(),
463-
carouselSize);
464-
steps.add(shifted);
464+
if (numberOfSteps <= 0 && defaultState.getLastFocalKeyline().cutoff > 0) {
465+
// If there are no steps, there still might be a cutoff focal item that we should shift into
466+
// view. Add a step that shifts all the keylines over to bring the last focal item into full
467+
// view.
468+
float cutoffs = defaultState.getLastFocalKeyline().cutoff;
469+
steps.add(
470+
shiftKeylinesAndCreateKeylineState(defaultState, originalStart - cutoffs, carouselSize));
465471
return steps;
466472
}
467473

468-
int start = defaultState.getLastFocalKeylineIndex();
469-
int end = lastNonAnchorKeylineIndex;
470-
int numberOfSteps = end - start;
471474
float cutoffs = 0;
472-
473475
for (int i = 0; i < numberOfSteps; i++) {
474476
KeylineState prevStepState = steps.get(steps.size() - 1);
475477
int itemOrigIndex = end - i;
@@ -503,6 +505,28 @@ private static List<KeylineState> getStateStepsEnd(
503505
return steps;
504506
}
505507

508+
/**
509+
* Creates a new, valid KeylineState that has the same order as {@code state} but with all
510+
* keylines shifted along the scrolling axis.
511+
*
512+
* @param state the state to shift
513+
* @param startOffset the point along the scrolling axis where keylines should start being added
514+
* from
515+
* @param carouselSize the size of the carousel container
516+
* @return a new {@link KeylineState} with the shifted keylines
517+
*/
518+
private static KeylineState shiftKeylinesAndCreateKeylineState(
519+
KeylineState state, float startOffset, float carouselSize) {
520+
return moveKeylineAndCreateKeylineState(
521+
state,
522+
0,
523+
0,
524+
startOffset,
525+
state.getFirstFocalKeylineIndex(),
526+
state.getLastFocalKeylineIndex(),
527+
carouselSize);
528+
}
529+
506530
/**
507531
* Creates a new, valid KeylineState from a list of keylines that have been re-arranged.
508532
*

‎lib/javatests/com/google/android/material/carousel/KeylineStateTest.java

+10-6
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,22 @@ public void testReverseKeylines_shouldReverse() {
113113
// Extra small items are 10F, Small items are 50F, large items are 100F
114114
KeylineState keylineState =
115115
new KeylineState.Builder(100F, recyclerWidth)
116-
.addKeyline(-5F, getKeylineMaskPercentage(10F, 100F), 10F)
116+
// left edge of xSmall item is -10 from left edge of carousel container
117+
.addKeyline(-5F, getKeylineMaskPercentage(10F, 100F), 10F, false, true)
117118
.addKeyline(50F, 0F, 100F, true)
118119
.addKeyline(125F, getKeylineMaskPercentage(50F, 100F), 50F)
119-
.addKeyline(155F, getKeylineMaskPercentage(10F, 100F), 10F)
120+
// right edge of xSmall item is 60 from right edge of carousel container
121+
.addKeyline(155F, getKeylineMaskPercentage(10F, 100F), 10F, false, true)
120122
.build();
121123

122124
KeylineState expectedState =
123125
new KeylineState.Builder(100F, recyclerWidth)
124-
.addKeyline(-5F, getKeylineMaskPercentage(10F, 100F), 10F)
125-
.addKeyline(25F, getKeylineMaskPercentage(50, 100F), 50F)
126-
.addKeyline(100F, 0F, 100F, true)
127-
.addKeyline(155F, getKeylineMaskPercentage(10F, 100F), 10F)
126+
// left edge of xSmall item is -60 from left of carousel container
127+
.addKeyline(-55F, getKeylineMaskPercentage(10F, 100F), 10F, false, true)
128+
.addKeyline(-25F, getKeylineMaskPercentage(50, 100F), 50F)
129+
.addKeyline(50F, 0F, 100F, true)
130+
// right edge of xSmall item is 10 from right of carousel container
131+
.addKeyline(105F, getKeylineMaskPercentage(10F, 100F), 10F, false, true)
128132
.build();
129133
KeylineState reversedState = KeylineState.reverse(keylineState, recyclerWidth);
130134

0 commit comments

Comments
 (0)
Please sign in to comment.