Skip to content

Commit

Permalink
[Carousel] Add left-aligned uncontained strategy
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 559197283
  • Loading branch information
imhappi committed Aug 23, 2023
1 parent 7d8681f commit 9d81cac
Show file tree
Hide file tree
Showing 11 changed files with 741 additions and 110 deletions.
Expand Up @@ -294,7 +294,8 @@ private void recalculateKeylineStateList(Recycler recycler) {
KeylineState keylineState = carouselStrategy.onFirstChildMeasuredWithMargins(this, firstChild);
keylineStateList =
KeylineStateList.from(
this, isLayoutRtl() ? KeylineState.reverse(keylineState) : keylineState);
this,
isLayoutRtl() ? KeylineState.reverse(keylineState, getContainerSize()) : keylineState);
}

/**
Expand Down
Expand Up @@ -101,8 +101,8 @@ static KeylineState createLeftAlignedKeylineState(
float largeMask = 0F;

KeylineState.Builder builder =
new KeylineState.Builder(arrangement.largeSize)
.addKeyline(extraSmallHeadCenterX, extraSmallMask, extraSmallChildWidth)
new KeylineState.Builder(arrangement.largeSize, availableSpace)
.addAnchorKeyline(extraSmallHeadCenterX, extraSmallMask, extraSmallChildWidth)
.addKeylineRange(
largeStartCenterX, largeMask, arrangement.largeSize, arrangement.largeCount, true);
if (arrangement.mediumCount > 0) {
Expand All @@ -112,7 +112,7 @@ static KeylineState createLeftAlignedKeylineState(
builder.addKeylineRange(
smallStartCenterX, smallMask, arrangement.smallSize, arrangement.smallCount);
}
builder.addKeyline(extraSmallTailCenterX, extraSmallMask, extraSmallChildWidth);
builder.addAnchorKeyline(extraSmallTailCenterX, extraSmallMask, extraSmallChildWidth);
return builder.build();
}

Expand Down Expand Up @@ -193,8 +193,8 @@ static KeylineState createCenterAlignedKeylineState(
float largeMask = 0F;

KeylineState.Builder builder =
new KeylineState.Builder(arrangement.largeSize)
.addKeyline(extraSmallHeadCenterX, extraSmallMask, extraSmallChildWidth);
new KeylineState.Builder(arrangement.largeSize, availableSpace)
.addAnchorKeyline(extraSmallHeadCenterX, extraSmallMask, extraSmallChildWidth);
if (arrangement.smallCount > 0) {
builder.addKeylineRange(
halfSmallStartCenterX,
Expand Down Expand Up @@ -229,7 +229,7 @@ static KeylineState createCenterAlignedKeylineState(
(int) Math.ceil(arrangement.smallCount / 2F));
}

builder.addKeyline(extraSmallTailCenterX, extraSmallMask, extraSmallChildWidth);
builder.addAnchorKeyline(extraSmallTailCenterX, extraSmallMask, extraSmallChildWidth);
return builder.build();
}

Expand Down
217 changes: 205 additions & 12 deletions lib/java/com/google/android/material/carousel/KeylineState.java
Expand Up @@ -16,8 +16,12 @@

package com.google.android.material.carousel;

import static java.lang.Math.max;
import static java.lang.Math.min;

import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.animation.AnimationUtils;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.ArrayList;
Expand Down Expand Up @@ -107,6 +111,30 @@ Keyline getLastKeyline() {
return keylines.get(keylines.size() - 1);
}

/** Returns the first non-anchor keyline. */
@Nullable
Keyline getFirstNonAnchorKeyline() {
for (int i = 0; i < keylines.size(); i++) {
Keyline keyline = keylines.get(i);
if (!keyline.isAnchor) {
return keyline;
}
}
return null;
}

/** Returns the last non-anchor keyline. */
@Nullable
Keyline getLastNonAnchorKeyline() {
for (int i = keylines.size()-1; i >= 0; i--) {
Keyline keyline = keylines.get(i);
if (!keyline.isAnchor) {
return keyline;
}
}
return null;
}

/**
* Linearly interpolate between two {@link KeylineState}s.
*
Expand Down Expand Up @@ -149,11 +177,14 @@ static KeylineState lerp(KeylineState from, KeylineState to, float progress) {
* <p>This is used to reverse a keyline state for RTL layouts.
*
* @param keylineState the {@link KeylineState} to reverse
* @param availableSpace the space in which the keylines calculate whether or not they are cut
* off.
* @return a new {@link KeylineState} that has all keylines reversed.
*/
static KeylineState reverse(KeylineState keylineState) {
static KeylineState reverse(KeylineState keylineState, float availableSpace) {

KeylineState.Builder builder = new KeylineState.Builder(keylineState.getItemSize());
KeylineState.Builder builder =
new KeylineState.Builder(keylineState.getItemSize(), availableSpace);

float start =
keylineState.getFirstKeyline().locOffset
Expand Down Expand Up @@ -198,6 +229,8 @@ static final class Builder {

private final float itemSize;

private final float availableSpace;

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

private float lastKeylineMaskedSize = 0F;

private int latestAnchorKeylineIndex = NO_INDEX;


/**
* Creates a new {@link KeylineState.Builder}.
*
* @param itemSize The size of a fully unmaksed item. This is the size that will be used by the
* carousel to measure and lay out all children, overriding each child's desired size.
* @param availableSpace The available space of the carousel the keylines calculate cutoffs by.
*/
Builder(float itemSize) {
Builder(float itemSize, float availableSpace) {
this.itemSize = itemSize;
this.availableSpace = availableSpace;
}

/**
* Adds a keyline along the scrolling axis where an object should be masked by the given {@code
* mask} and positioned at {@code offsetLoc}.
* Adds a non-anchor keyline along the scrolling axis where an object should be masked by the
* given {@code mask} and positioned at {@code offsetLoc}. Non-anchor keylines shift when
* keylines shift due to scrolling.
*
* <p>Note that calls to {@link #addKeyline(float, float, float, boolean)} and {@link
* #addKeylineRange(float, float, float, int)} are added in order. Typically, this means
* keylines should be added in order of ascending {@code offsetLoc}.
*
* @param offsetLoc The location of this keyline along the scrolling axis. An offsetLoc of 0
* will be at the start of the scroll container.
* @param mask The percentage of a child's full size that it should be masked by when its center
* is at {@code offsetLoc}. 0 is fully unmasked and 1 is fully masked.
* @param maskedItemSize The total size of this item when masked. This might differ from {@code
* itemSize - (itemSize * mask)} depending on how margins are included in the {@code mask}.
* @param isFocal Whether this keyline is considered part of the focal range. Typically, this is
* when {@code mask} is equal to 0.
*/
@NonNull
@CanIgnoreReturnValue
Builder addKeyline(
float offsetLoc,
@FloatRange(from = 0.0F, to = 1.0F) float mask,
float maskedItemSize,
boolean isFocal) {
return addKeyline(offsetLoc, mask, maskedItemSize, isFocal, /* isAnchor= */ false);
}

/**
* Adds a non-anchor keyline along the scrolling axis where an object should be masked by the
* given {@code mask} and positioned at {@code offsetLoc}.
*
* @see #addKeyline(float, float, float, boolean)
* @see #addKeyline(float, float, float, boolean, boolean)
*/
@NonNull
@CanIgnoreReturnValue
Expand All @@ -235,9 +301,14 @@ Builder addKeyline(
* Adds a keyline along the scrolling axis where an object should be masked by the given {@code
* mask} and positioned at {@code offsetLoc}.
*
* <p>Note that calls to {@link #addKeyline(float, float, float, boolean)} and {@link
* <p>Note that calls to {@link #addKeyline(float, float, float, boolean, boolean)} and {@link
* #addKeylineRange(float, float, float, int)} are added in order. Typically, this means
* keylines should be added in order of ascending {@code offsetLoc}.
* keylines should be added in order of ascending {@code offsetLoc}. The first and last keylines
* added are 'anchor' keylines that mark the start and ends of the keylines. These keylines do
* not shift when scrolled.
*
* <p>Note also that {@code isFocal} and {@code isAnchor} cannot be true at the same time as
* anchor keylines refer to keylines offscreen that dictate the ends of the keylines.
*
* @param offsetLoc The location of this keyline along the scrolling axis. An offsetLoc of 0
* will be at the start of the scroll container.
Expand All @@ -247,19 +318,36 @@ Builder addKeyline(
* itemSize - (itemSize * mask)} depending on how margins are included in the {@code mask}.
* @param isFocal Whether this keyline is considered part of the focal range. Typically, this is
* when {@code mask} is equal to 0.
* @param isAnchor Whether this keyline is an anchor keyline. Anchor keylines do not shift when
* keylines are shifted.
* @param cutoff How much the keyline item is out the bounds of the available space.
*/
@NonNull
@CanIgnoreReturnValue
Builder addKeyline(
float offsetLoc,
@FloatRange(from = 0.0F, to = 1.0F) float mask,
float maskedItemSize,
boolean isFocal) {
boolean isFocal,
boolean isAnchor,
float cutoff) {
if (maskedItemSize <= 0F) {
return this;
}
if (isAnchor) {
if (isFocal) {
throw new IllegalArgumentException(
"Anchor keylines cannot be focal.");
}
if (latestAnchorKeylineIndex != NO_INDEX && latestAnchorKeylineIndex != 0) {
throw new IllegalArgumentException(
"Anchor keylines must be either the first or last keyline.");
}
latestAnchorKeylineIndex = tmpKeylines.size();
}

Keyline tmpKeyline = new Keyline(UNKNOWN_LOC, offsetLoc, mask, maskedItemSize);
Keyline tmpKeyline =
new Keyline(UNKNOWN_LOC, offsetLoc, mask, maskedItemSize, isAnchor, cutoff);
if (isFocal) {
if (tmpFirstFocalKeyline == null) {
tmpFirstFocalKeyline = tmpKeyline;
Expand Down Expand Up @@ -294,6 +382,81 @@ Builder addKeyline(
return this;
}

/**
* Adds a keyline along the scrolling axis where an object should be masked by the given {@code
* mask} and positioned at {@code offsetLoc}. This method also calculates the amount that a
* keyline may be cut off by the bounds of the available space given.
*
* <p>Note that calls to {@link #addKeyline(float, float, float, boolean, boolean)} and {@link
* #addKeylineRange(float, float, float, int)} are added in order. Typically, this means
* keylines should be added in order of ascending {@code offsetLoc}. The first and last keylines
* added are 'anchor' keylines that mark the start and ends of the keylines. These keylines do
* not shift when scrolled.
*
* <p>Note also that {@code isFocal} and {@code isAnchor} cannot be true at the same time as
* anchor keylines refer to keylines offscreen that dictate the ends of the keylines.
*
* @param offsetLoc The location of this keyline along the scrolling axis. An offsetLoc of 0
* will be at the start of the scroll container.
* @param mask The percentage of a child's full size that it should be masked by when its center
* is at {@code offsetLoc}. 0 is fully unmasked and 1 is fully masked.
* @param maskedItemSize The total size of this item when masked. This might differ from {@code
* itemSize - (itemSize * mask)} depending on how margins are included in the {@code mask}.
* @param isFocal Whether this keyline is considered part of the focal range. Typically, this is
* when {@code mask} is equal to 0.
* @param isAnchor Whether this keyline is an anchor keyline. Anchor keylines do not shift when
* keylines are shifted.
*/
@NonNull
@CanIgnoreReturnValue
Builder addKeyline(
float offsetLoc,
@FloatRange(from = 0.0F, to = 1.0F) float mask,
float maskedItemSize,
boolean isFocal,
boolean isAnchor) {
float cutoff = 0;
// Calculate if the item will be cut off on either side. Currently we do not support an item
// cut off on both sides as we do not not support that use case. If an item is cut off on both
// sides, only the end cutoff will be included in the cutoff.
float keylineStart = offsetLoc - maskedItemSize/2F;
float keylineEnd = offsetLoc + maskedItemSize/2F;
if (keylineEnd > availableSpace) {
cutoff = Math.abs(keylineEnd - max(keylineEnd - maskedItemSize, availableSpace));
} else if (keylineStart < 0) {
cutoff = Math.abs(keylineStart - min(keylineStart + maskedItemSize, 0));
}

return addKeyline(offsetLoc, mask, maskedItemSize, isFocal, isAnchor, cutoff);
}

/**
* Adds an anchor keyline along the scrolling axis where an object should be masked by the given
* {@code mask} and positioned at {@code offsetLoc}.
*
* <p>Anchor keylines are keylines that are added to increase motion of carousel items going
* out of bounds of the carousel, and are 'anchored' (ie. does not shift). These keylines must
* be at the start or end of all keylines.
*
* <p>Note that calls to {@link #addKeyline(float, float, float, boolean)} and {@link
* #addKeylineRange(float, float, float, int)} are added in order. This method should be called
* first, or last of all the `addKeyline` calls.
*
* @param offsetLoc The location of this keyline along the scrolling axis. An offsetLoc of 0
* will be at the start of the scroll container.
* @param mask The percentage of a child's full size that it should be masked by when its center
* is at {@code offsetLoc}. 0 is fully unmasked and 1 is fully masked.
* @param maskedItemSize The total size of this item when masked. This might differ from {@code
* itemSize - (itemSize * mask)} depending on how margins are included in the {@code mask}.
*/
@NonNull
@CanIgnoreReturnValue
Builder addAnchorKeyline(
float offsetLoc, @FloatRange(from = 0.0F, to = 1.0F) float mask, float maskedItemSize) {
return addKeyline(
offsetLoc, mask, maskedItemSize, /* isFocal= */ false, /* isAnchor= */ true);
}

/**
* Adds a range of keylines along the scrolling axis where an item should be masked by {@code
* mask} when its center is between {@code offsetLoc} and {@code offsetLoc + (maskedItemSize *
Expand Down Expand Up @@ -366,7 +529,9 @@ KeylineState build() {
tmpFirstFocalKeyline.locOffset, itemSize, firstFocalKeylineIndex, i),
tmpKeyline.locOffset,
tmpKeyline.mask,
tmpKeyline.maskedItemSize);
tmpKeyline.maskedItemSize,
tmpKeyline.isAnchor,
tmpKeyline.cutoff);
keylines.add(keyline);
}

Expand Down Expand Up @@ -401,9 +566,11 @@ static final class Keyline {
final float locOffset;
final float mask;
final float maskedItemSize;
final boolean isAnchor;
final float cutoff;

/**
* Creates a keyline along a scroll axis.
* Creates a non-anchor keyline along a scroll axis.
*
* @param loc Where this item will be along the scroll axis if it were laid out end-to-end when
* it should be in the state defined by {@code locOffset} and {@code mask}.
Expand All @@ -414,10 +581,36 @@ static final class Keyline {
* @param maskedItemSize The size of this item when masked.
*/
Keyline(float loc, float locOffset, float mask, float maskedItemSize) {
this(loc, locOffset, mask, maskedItemSize, /* isAnchor= */ false, 0);
}

/**
* Creates a keyline along a scroll axis.
*
* @param loc Where this item will be along the scroll axis if it were laid out end-to-end when
* it should be in the state defined by {@code locOffset} and {@code mask}.
* @param locOffset The location within the carousel where an item should be when its center is
* at {@code loc}.
* @param mask The percentage of this items full size that it should be masked by when its
* center is at {@code loc}.
* @param maskedItemSize The size of this item when masked.
* @param isAnchor Whether or not the keyline is an anchor keyline (keylines at the end that do
* not shift).
* @param cutoff The amount by which the keyline item is cut off by the bounds of the carousel.
*/
Keyline(
float loc,
float locOffset,
float mask,
float maskedItemSize,
boolean isAnchor,
float cutoff) {
this.loc = loc;
this.locOffset = locOffset;
this.mask = mask;
this.maskedItemSize = maskedItemSize;
this.isAnchor = isAnchor;
this.cutoff = cutoff;
}

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

0 comments on commit 9d81cac

Please sign in to comment.