Skip to content


[Carousel] Tweak uncontained strategy logic to adjust medium size ite…
Browse files Browse the repository at this point in the history
…ms to improve motion

PiperOrigin-RevId: 566720825
  • Loading branch information
imhappi authored and dsn5ft committed Sep 19, 2023
1 parent c4ae01a commit 93660d4
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 52 deletions.
Expand Up @@ -46,6 +46,8 @@
public final class UncontainedCarouselStrategy extends CarouselStrategy {

private static final float MEDIUM_LARGE_ITEM_PERCENTAGE_THRESHOLD = 0.85F;

public UncontainedCarouselStrategy() {
Expand All @@ -70,9 +72,8 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
float xSmallChildSize = getExtraSmallSize(child.getContext()) + childMargins;

// Calculate how much space there is remaining after squeezing in as many large items as we can.
int largeCount = (int) Math.floor(availableSpace/largeChildSize);
int largeCount = max(1, (int) Math.floor(availableSpace/largeChildSize));
float remainingSpace = availableSpace - largeCount*largeChildSize;
int mediumCount = 0;
boolean isCenter = carousel.getCarouselAlignment() == CarouselLayoutManager.ALIGNMENT_CENTER;

if (isCenter) {
Expand All @@ -98,20 +99,15 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul

// If the keyline location for the next large size would be within the remaining space,
// then we can place a large child there as the last non-anchor keyline because visually
// keylines will become smaller as it goes past the large keyline location.
// Otherwise, we want to add a medium item instead so that visually there will still
// be the effect of the item getting smaller closer to the end.
if (remainingSpace > largeChildSize/2F) {
largeCount += 1;
} else {
int mediumCount = 0;

if (remainingSpace > 0) {
mediumCount = 1;
// We want the medium size to be large enough to be at least 1/3 of the way cut
// off.
mediumChildSize = max(remainingSpace + remainingSpace/2F, mediumChildSize);

// Calculate the medium size so that it fulfils certain criteria.
mediumChildSize = calculateMediumChildSize(mediumChildSize, largeChildSize, remainingSpace);

return createLeftAlignedKeylineState(
Expand All @@ -123,6 +119,35 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul

* Calculates a size of a medium child in the carousel that is not bigger than the large child
* size, and attempts to be small enough such that there is a size disparity between the medium
* and large sizes, but large enough to have a sufficient percentage cut off.
private float calculateMediumChildSize(
float mediumChildSize, float largeChildSize, float remainingSpace) {
// With the remaining space, we want to add a 'medium' item that gets sufficiently cut off
// but is close enough to the anchor keyline such that there is a range of motion.
// Ideally the medium child size is large enough such that a third is cut off.
mediumChildSize = max(remainingSpace * 1.5f, mediumChildSize);
// The size we wish to limit the medium size to.
float largeItemThreshold = largeChildSize * MEDIUM_LARGE_ITEM_PERCENTAGE_THRESHOLD;

// If the medium child is larger than the threshold percentage of the large child size,
// it's too similar and won't create sufficient motion when scrolling items between the large
// items and the medium item.
if (mediumChildSize > largeItemThreshold) {
// Choose whichever is bigger between the maximum threshold of the medium child size, or
// a size such that only 20% of the space is cut off.
mediumChildSize =
max(largeItemThreshold, remainingSpace * 1.2F);

// Ensure that the final medium size is not larger than the large size.
mediumChildSize = min(largeChildSize, mediumChildSize);
return mediumChildSize;

private KeylineState createCenterAlignedKeylineState(
float availableSpace,
float childMargins,
Expand Down Expand Up @@ -171,54 +196,38 @@ private KeylineState createLeftAlignedKeylineState(
int mediumCount,
float xSmallSize) {

// Make the left anchor size half the cut off item size to make the motion at the left closer
// to the right where the cut off is.
float leftAnchorSize = max(xSmallSize, mediumSize * 0.5F);
float leftAnchorMask = getChildMaskPercentage(leftAnchorSize, largeSize, childMargins);
float extraSmallMask =
getChildMaskPercentage(xSmallSize, largeSize, childMargins);
float mediumMask =
getChildMaskPercentage(mediumSize, largeSize, childMargins);
float largeMask = 0F;

float start = 0F;
float extraSmallHeadCenterX = start - (xSmallSize / 2F);
float leftAnchorCenterX = start - (leftAnchorSize / 2F);

float largeStartCenterX = largeSize/2F;
// We exclude one large item from the large count so we can calculate the correct
// cutoff value for the last large item if it is getting cut off.
int excludedLargeCount = max(largeCount - 1, 1);
start += excludedLargeCount * largeSize;
// Where to start laying the last possibly-cutoff large item.
float lastEndStart = start + largeSize / 2F;
start += largeCount * largeSize;

// Add xsmall keyline, and then if there is more than 1 large keyline, add
// however many large keylines there are except for the last one that may be cut off.
KeylineState.Builder builder =
new KeylineState.Builder(largeSize, availableSpace)
.addAnchorKeyline(extraSmallHeadCenterX, extraSmallMask, xSmallSize)
.addAnchorKeyline(leftAnchorCenterX, leftAnchorMask, leftAnchorSize)
/* isFocal= */ true);

// If we have more than 1 large item, then here we include the last large item that is
// possibly getting cut off.
if (largeCount > 1) {
start += largeSize;
/* isFocal= */ true);

if (mediumCount > 0) {
float mediumCenterX = start + mediumSize / 2F;
start += mediumSize;
/* isFocal= */ false);
builder.addKeyline(mediumCenterX, mediumMask, mediumSize, /* isFocal= */ false);

float xSmallCenterX = start + getExtraSmallSize(context) / 2F;
Expand Down
Expand Up @@ -44,16 +44,14 @@ public void testLargeItem_withFullCarouselWidth() {
float xSmallSize =

// A fullscreen layout should be [xSmall-large-xSmall-xSmall] where the xSmall items are
// A fullscreen layout should be [xSmall-large-xSmall] where the xSmall items are
// outside the bounds of the carousel container and the large item takes up the
// containers full width.
.isEqualTo(carousel.getContainerWidth() + xSmallSize / 2F);
.isGreaterThan((float) carousel.getContainerWidth());

Expand All @@ -79,10 +77,9 @@ public void testLargeItem_largerThanFullCarouselWidth() {
public void testRemainingSpaceWithItemSize_fitsItemWithThirdCutoff() {
Carousel carousel = createCarouselWithWidth(400);
UncontainedCarouselStrategy config = new UncontainedCarouselStrategy();
// With size 125px, 3 large items can fit with in 400px, with 25px left over which is less than
// half 125px. This means that a large keyline will not fit in the remaining space
// such that any motion is seen when scrolling past the keyline, so a medium item
// should be added.
// With size 125px, 3 large items can fit with in 400px, with 25px left. 25px * 3 = 75px, which
// will be the size of the medium item since it can be a third cut off and it is less than the
// threshold percentage * large item size.
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 125, 400);

KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view);
Expand All @@ -101,24 +98,26 @@ public void testRemainingSpaceWithItemSize_fitsItemWithThirdCutoff() {

public void testRemainingSpaceWithItemSize_fitsLargeItemWithCutoff() {
public void testRemainingSpaceWithItemSize_fitsMediumItemWithCutoff() {
Carousel carousel = createCarouselWithWidth(400);
UncontainedCarouselStrategy config = new UncontainedCarouselStrategy();
int itemSize = 105;
// With size 105px, 3 large items can fit with in 400px, with 85px left over which is more than
// half 105px. This means that an extra large keyline will fit in the remaining space such
// that motion is seen when scrolling past the keyline, so it should add a large item.
// With size 105px, 3 large items can fit with in 400px, with 85px left over. 85*3 = 255 which
// is well over the size of the large item, so the medium size will be limited to whichever is
// larger between 85% of the large size, or 110% of the remainingSpace to make it at most
// 10% cut off.
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), itemSize, 400);

KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view);

// The layout should be [xSmall-large-large-large-large-xSmall]
// The layout should be [xSmall-large-large-large-medium-xSmall]
// remainingSpace * 120%
.isGreaterThan((float) carousel.getContainerWidth());
Expand Down

0 comments on commit 93660d4

Please sign in to comment.