1616
1717package com .google .android .material .carousel ;
1818
19+ import static java .lang .Math .max ;
20+ import static java .lang .Math .min ;
21+
1922import androidx .annotation .FloatRange ;
2023import androidx .annotation .NonNull ;
24+ import androidx .annotation .Nullable ;
2125import com .google .android .material .animation .AnimationUtils ;
2226import com .google .errorprone .annotations .CanIgnoreReturnValue ;
2327import 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