Skip to content

Commit 9d81cac

Browse files
committed
[Carousel] Add left-aligned uncontained strategy
PiperOrigin-RevId: 559197283
1 parent 7d8681f commit 9d81cac

File tree

11 files changed

+741
-110
lines changed

11 files changed

+741
-110
lines changed

lib/java/com/google/android/material/carousel/CarouselLayoutManager.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,8 @@ private void recalculateKeylineStateList(Recycler recycler) {
294294
KeylineState keylineState = carouselStrategy.onFirstChildMeasuredWithMargins(this, firstChild);
295295
keylineStateList =
296296
KeylineStateList.from(
297-
this, isLayoutRtl() ? KeylineState.reverse(keylineState) : keylineState);
297+
this,
298+
isLayoutRtl() ? KeylineState.reverse(keylineState, getContainerSize()) : keylineState);
298299
}
299300

300301
/**

lib/java/com/google/android/material/carousel/CarouselStrategyHelper.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ static KeylineState createLeftAlignedKeylineState(
101101
float largeMask = 0F;
102102

103103
KeylineState.Builder builder =
104-
new KeylineState.Builder(arrangement.largeSize)
105-
.addKeyline(extraSmallHeadCenterX, extraSmallMask, extraSmallChildWidth)
104+
new KeylineState.Builder(arrangement.largeSize, availableSpace)
105+
.addAnchorKeyline(extraSmallHeadCenterX, extraSmallMask, extraSmallChildWidth)
106106
.addKeylineRange(
107107
largeStartCenterX, largeMask, arrangement.largeSize, arrangement.largeCount, true);
108108
if (arrangement.mediumCount > 0) {
@@ -112,7 +112,7 @@ static KeylineState createLeftAlignedKeylineState(
112112
builder.addKeylineRange(
113113
smallStartCenterX, smallMask, arrangement.smallSize, arrangement.smallCount);
114114
}
115-
builder.addKeyline(extraSmallTailCenterX, extraSmallMask, extraSmallChildWidth);
115+
builder.addAnchorKeyline(extraSmallTailCenterX, extraSmallMask, extraSmallChildWidth);
116116
return builder.build();
117117
}
118118

@@ -193,8 +193,8 @@ static KeylineState createCenterAlignedKeylineState(
193193
float largeMask = 0F;
194194

195195
KeylineState.Builder builder =
196-
new KeylineState.Builder(arrangement.largeSize)
197-
.addKeyline(extraSmallHeadCenterX, extraSmallMask, extraSmallChildWidth);
196+
new KeylineState.Builder(arrangement.largeSize, availableSpace)
197+
.addAnchorKeyline(extraSmallHeadCenterX, extraSmallMask, extraSmallChildWidth);
198198
if (arrangement.smallCount > 0) {
199199
builder.addKeylineRange(
200200
halfSmallStartCenterX,
@@ -229,7 +229,7 @@ static KeylineState createCenterAlignedKeylineState(
229229
(int) Math.ceil(arrangement.smallCount / 2F));
230230
}
231231

232-
builder.addKeyline(extraSmallTailCenterX, extraSmallMask, extraSmallChildWidth);
232+
builder.addAnchorKeyline(extraSmallTailCenterX, extraSmallMask, extraSmallChildWidth);
233233
return builder.build();
234234
}
235235

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

Lines changed: 205 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616

1717
package com.google.android.material.carousel;
1818

19+
import static java.lang.Math.max;
20+
import static java.lang.Math.min;
21+
1922
import androidx.annotation.FloatRange;
2023
import androidx.annotation.NonNull;
24+
import androidx.annotation.Nullable;
2125
import com.google.android.material.animation.AnimationUtils;
2226
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2327
import java.util.ArrayList;
@@ -107,6 +111,30 @@ Keyline getLastKeyline() {
107111
return keylines.get(keylines.size() - 1);
108112
}
109113

114+
/** Returns the first non-anchor keyline. */
115+
@Nullable
116+
Keyline getFirstNonAnchorKeyline() {
117+
for (int i = 0; i < keylines.size(); i++) {
118+
Keyline keyline = keylines.get(i);
119+
if (!keyline.isAnchor) {
120+
return keyline;
121+
}
122+
}
123+
return null;
124+
}
125+
126+
/** Returns the last non-anchor keyline. */
127+
@Nullable
128+
Keyline getLastNonAnchorKeyline() {
129+
for (int i = keylines.size()-1; i >= 0; i--) {
130+
Keyline keyline = keylines.get(i);
131+
if (!keyline.isAnchor) {
132+
return keyline;
133+
}
134+
}
135+
return null;
136+
}
137+
110138
/**
111139
* Linearly interpolate between two {@link KeylineState}s.
112140
*
@@ -149,11 +177,14 @@ static KeylineState lerp(KeylineState from, KeylineState to, float progress) {
149177
* <p>This is used to reverse a keyline state for RTL layouts.
150178
*
151179
* @param keylineState the {@link KeylineState} to reverse
180+
* @param availableSpace the space in which the keylines calculate whether or not they are cut
181+
* off.
152182
* @return a new {@link KeylineState} that has all keylines reversed.
153183
*/
154-
static KeylineState reverse(KeylineState keylineState) {
184+
static KeylineState reverse(KeylineState keylineState, float availableSpace) {
155185

156-
KeylineState.Builder builder = new KeylineState.Builder(keylineState.getItemSize());
186+
KeylineState.Builder builder =
187+
new KeylineState.Builder(keylineState.getItemSize(), availableSpace);
157188

158189
float start =
159190
keylineState.getFirstKeyline().locOffset
@@ -198,6 +229,8 @@ static final class Builder {
198229

199230
private final float itemSize;
200231

232+
private final float availableSpace;
233+
201234
// A list of keylines that hold all values except the Keyline#loc which needs to be calculated
202235
// in the build method.
203236
private final List<Keyline> tmpKeylines = new ArrayList<>();
@@ -208,21 +241,54 @@ static final class Builder {
208241

209242
private float lastKeylineMaskedSize = 0F;
210243

244+
private int latestAnchorKeylineIndex = NO_INDEX;
245+
246+
211247
/**
212248
* Creates a new {@link KeylineState.Builder}.
213249
*
214250
* @param itemSize The size of a fully unmaksed item. This is the size that will be used by the
215251
* carousel to measure and lay out all children, overriding each child's desired size.
252+
* @param availableSpace The available space of the carousel the keylines calculate cutoffs by.
216253
*/
217-
Builder(float itemSize) {
254+
Builder(float itemSize, float availableSpace) {
218255
this.itemSize = itemSize;
256+
this.availableSpace = availableSpace;
219257
}
220258

221259
/**
222-
* Adds a keyline along the scrolling axis where an object should be masked by the given {@code
223-
* mask} and positioned at {@code offsetLoc}.
260+
* Adds a non-anchor keyline along the scrolling axis where an object should be masked by the
261+
* given {@code mask} and positioned at {@code offsetLoc}. Non-anchor keylines shift when
262+
* keylines shift due to scrolling.
263+
*
264+
* <p>Note that calls to {@link #addKeyline(float, float, float, boolean)} and {@link
265+
* #addKeylineRange(float, float, float, int)} are added in order. Typically, this means
266+
* keylines should be added in order of ascending {@code offsetLoc}.
267+
*
268+
* @param offsetLoc The location of this keyline along the scrolling axis. An offsetLoc of 0
269+
* will be at the start of the scroll container.
270+
* @param mask The percentage of a child's full size that it should be masked by when its center
271+
* is at {@code offsetLoc}. 0 is fully unmasked and 1 is fully masked.
272+
* @param maskedItemSize The total size of this item when masked. This might differ from {@code
273+
* itemSize - (itemSize * mask)} depending on how margins are included in the {@code mask}.
274+
* @param isFocal Whether this keyline is considered part of the focal range. Typically, this is
275+
* when {@code mask} is equal to 0.
276+
*/
277+
@NonNull
278+
@CanIgnoreReturnValue
279+
Builder addKeyline(
280+
float offsetLoc,
281+
@FloatRange(from = 0.0F, to = 1.0F) float mask,
282+
float maskedItemSize,
283+
boolean isFocal) {
284+
return addKeyline(offsetLoc, mask, maskedItemSize, isFocal, /* isAnchor= */ false);
285+
}
286+
287+
/**
288+
* Adds a non-anchor keyline along the scrolling axis where an object should be masked by the
289+
* given {@code mask} and positioned at {@code offsetLoc}.
224290
*
225-
* @see #addKeyline(float, float, float, boolean)
291+
* @see #addKeyline(float, float, float, boolean, boolean)
226292
*/
227293
@NonNull
228294
@CanIgnoreReturnValue
@@ -235,9 +301,14 @@ Builder addKeyline(
235301
* Adds a keyline along the scrolling axis where an object should be masked by the given {@code
236302
* mask} and positioned at {@code offsetLoc}.
237303
*
238-
* <p>Note that calls to {@link #addKeyline(float, float, float, boolean)} and {@link
304+
* <p>Note that calls to {@link #addKeyline(float, float, float, boolean, boolean)} and {@link
239305
* #addKeylineRange(float, float, float, int)} are added in order. Typically, this means
240-
* keylines should be added in order of ascending {@code offsetLoc}.
306+
* keylines should be added in order of ascending {@code offsetLoc}. The first and last keylines
307+
* added are 'anchor' keylines that mark the start and ends of the keylines. These keylines do
308+
* not shift when scrolled.
309+
*
310+
* <p>Note also that {@code isFocal} and {@code isAnchor} cannot be true at the same time as
311+
* anchor keylines refer to keylines offscreen that dictate the ends of the keylines.
241312
*
242313
* @param offsetLoc The location of this keyline along the scrolling axis. An offsetLoc of 0
243314
* will be at the start of the scroll container.
@@ -247,19 +318,36 @@ Builder addKeyline(
247318
* itemSize - (itemSize * mask)} depending on how margins are included in the {@code mask}.
248319
* @param isFocal Whether this keyline is considered part of the focal range. Typically, this is
249320
* when {@code mask} is equal to 0.
321+
* @param isAnchor Whether this keyline is an anchor keyline. Anchor keylines do not shift when
322+
* keylines are shifted.
323+
* @param cutoff How much the keyline item is out the bounds of the available space.
250324
*/
251325
@NonNull
252326
@CanIgnoreReturnValue
253327
Builder addKeyline(
254328
float offsetLoc,
255329
@FloatRange(from = 0.0F, to = 1.0F) float mask,
256330
float maskedItemSize,
257-
boolean isFocal) {
331+
boolean isFocal,
332+
boolean isAnchor,
333+
float cutoff) {
258334
if (maskedItemSize <= 0F) {
259335
return this;
260336
}
337+
if (isAnchor) {
338+
if (isFocal) {
339+
throw new IllegalArgumentException(
340+
"Anchor keylines cannot be focal.");
341+
}
342+
if (latestAnchorKeylineIndex != NO_INDEX && latestAnchorKeylineIndex != 0) {
343+
throw new IllegalArgumentException(
344+
"Anchor keylines must be either the first or last keyline.");
345+
}
346+
latestAnchorKeylineIndex = tmpKeylines.size();
347+
}
261348

262-
Keyline tmpKeyline = new Keyline(UNKNOWN_LOC, offsetLoc, mask, maskedItemSize);
349+
Keyline tmpKeyline =
350+
new Keyline(UNKNOWN_LOC, offsetLoc, mask, maskedItemSize, isAnchor, cutoff);
263351
if (isFocal) {
264352
if (tmpFirstFocalKeyline == null) {
265353
tmpFirstFocalKeyline = tmpKeyline;
@@ -294,6 +382,81 @@ Builder addKeyline(
294382
return this;
295383
}
296384

385+
/**
386+
* Adds a keyline along the scrolling axis where an object should be masked by the given {@code
387+
* mask} and positioned at {@code offsetLoc}. This method also calculates the amount that a
388+
* keyline may be cut off by the bounds of the available space given.
389+
*
390+
* <p>Note that calls to {@link #addKeyline(float, float, float, boolean, boolean)} and {@link
391+
* #addKeylineRange(float, float, float, int)} are added in order. Typically, this means
392+
* keylines should be added in order of ascending {@code offsetLoc}. The first and last keylines
393+
* added are 'anchor' keylines that mark the start and ends of the keylines. These keylines do
394+
* not shift when scrolled.
395+
*
396+
* <p>Note also that {@code isFocal} and {@code isAnchor} cannot be true at the same time as
397+
* anchor keylines refer to keylines offscreen that dictate the ends of the keylines.
398+
*
399+
* @param offsetLoc The location of this keyline along the scrolling axis. An offsetLoc of 0
400+
* will be at the start of the scroll container.
401+
* @param mask The percentage of a child's full size that it should be masked by when its center
402+
* is at {@code offsetLoc}. 0 is fully unmasked and 1 is fully masked.
403+
* @param maskedItemSize The total size of this item when masked. This might differ from {@code
404+
* itemSize - (itemSize * mask)} depending on how margins are included in the {@code mask}.
405+
* @param isFocal Whether this keyline is considered part of the focal range. Typically, this is
406+
* when {@code mask} is equal to 0.
407+
* @param isAnchor Whether this keyline is an anchor keyline. Anchor keylines do not shift when
408+
* keylines are shifted.
409+
*/
410+
@NonNull
411+
@CanIgnoreReturnValue
412+
Builder addKeyline(
413+
float offsetLoc,
414+
@FloatRange(from = 0.0F, to = 1.0F) float mask,
415+
float maskedItemSize,
416+
boolean isFocal,
417+
boolean isAnchor) {
418+
float cutoff = 0;
419+
// Calculate if the item will be cut off on either side. Currently we do not support an item
420+
// cut off on both sides as we do not not support that use case. If an item is cut off on both
421+
// sides, only the end cutoff will be included in the cutoff.
422+
float keylineStart = offsetLoc - maskedItemSize/2F;
423+
float keylineEnd = offsetLoc + maskedItemSize/2F;
424+
if (keylineEnd > availableSpace) {
425+
cutoff = Math.abs(keylineEnd - max(keylineEnd - maskedItemSize, availableSpace));
426+
} else if (keylineStart < 0) {
427+
cutoff = Math.abs(keylineStart - min(keylineStart + maskedItemSize, 0));
428+
}
429+
430+
return addKeyline(offsetLoc, mask, maskedItemSize, isFocal, isAnchor, cutoff);
431+
}
432+
433+
/**
434+
* Adds an anchor keyline along the scrolling axis where an object should be masked by the given
435+
* {@code mask} and positioned at {@code offsetLoc}.
436+
*
437+
* <p>Anchor keylines are keylines that are added to increase motion of carousel items going
438+
* out of bounds of the carousel, and are 'anchored' (ie. does not shift). These keylines must
439+
* be at the start or end of all keylines.
440+
*
441+
* <p>Note that calls to {@link #addKeyline(float, float, float, boolean)} and {@link
442+
* #addKeylineRange(float, float, float, int)} are added in order. This method should be called
443+
* first, or last of all the `addKeyline` calls.
444+
*
445+
* @param offsetLoc The location of this keyline along the scrolling axis. An offsetLoc of 0
446+
* will be at the start of the scroll container.
447+
* @param mask The percentage of a child's full size that it should be masked by when its center
448+
* is at {@code offsetLoc}. 0 is fully unmasked and 1 is fully masked.
449+
* @param maskedItemSize The total size of this item when masked. This might differ from {@code
450+
* itemSize - (itemSize * mask)} depending on how margins are included in the {@code mask}.
451+
*/
452+
@NonNull
453+
@CanIgnoreReturnValue
454+
Builder addAnchorKeyline(
455+
float offsetLoc, @FloatRange(from = 0.0F, to = 1.0F) float mask, float maskedItemSize) {
456+
return addKeyline(
457+
offsetLoc, mask, maskedItemSize, /* isFocal= */ false, /* isAnchor= */ true);
458+
}
459+
297460
/**
298461
* Adds a range of keylines along the scrolling axis where an item should be masked by {@code
299462
* mask} when its center is between {@code offsetLoc} and {@code offsetLoc + (maskedItemSize *
@@ -366,7 +529,9 @@ KeylineState build() {
366529
tmpFirstFocalKeyline.locOffset, itemSize, firstFocalKeylineIndex, i),
367530
tmpKeyline.locOffset,
368531
tmpKeyline.mask,
369-
tmpKeyline.maskedItemSize);
532+
tmpKeyline.maskedItemSize,
533+
tmpKeyline.isAnchor,
534+
tmpKeyline.cutoff);
370535
keylines.add(keyline);
371536
}
372537

@@ -401,9 +566,11 @@ static final class Keyline {
401566
final float locOffset;
402567
final float mask;
403568
final float maskedItemSize;
569+
final boolean isAnchor;
570+
final float cutoff;
404571

405572
/**
406-
* Creates a keyline along a scroll axis.
573+
* Creates a non-anchor keyline along a scroll axis.
407574
*
408575
* @param loc Where this item will be along the scroll axis if it were laid out end-to-end when
409576
* it should be in the state defined by {@code locOffset} and {@code mask}.
@@ -414,10 +581,36 @@ static final class Keyline {
414581
* @param maskedItemSize The size of this item when masked.
415582
*/
416583
Keyline(float loc, float locOffset, float mask, float maskedItemSize) {
584+
this(loc, locOffset, mask, maskedItemSize, /* isAnchor= */ false, 0);
585+
}
586+
587+
/**
588+
* Creates a keyline along a scroll axis.
589+
*
590+
* @param loc Where this item will be along the scroll axis if it were laid out end-to-end when
591+
* it should be in the state defined by {@code locOffset} and {@code mask}.
592+
* @param locOffset The location within the carousel where an item should be when its center is
593+
* at {@code loc}.
594+
* @param mask The percentage of this items full size that it should be masked by when its
595+
* center is at {@code loc}.
596+
* @param maskedItemSize The size of this item when masked.
597+
* @param isAnchor Whether or not the keyline is an anchor keyline (keylines at the end that do
598+
* not shift).
599+
* @param cutoff The amount by which the keyline item is cut off by the bounds of the carousel.
600+
*/
601+
Keyline(
602+
float loc,
603+
float locOffset,
604+
float mask,
605+
float maskedItemSize,
606+
boolean isAnchor,
607+
float cutoff) {
417608
this.loc = loc;
418609
this.locOffset = locOffset;
419610
this.mask = mask;
420611
this.maskedItemSize = maskedItemSize;
612+
this.isAnchor = isAnchor;
613+
this.cutoff = cutoff;
421614
}
422615

423616
/** Linearly interpolates between two keylines and returns the interpolated object. */

0 commit comments

Comments
 (0)