Skip to content

Commit

Permalink
[Carousel] Center aligned uncontained carousel
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 559215330
  • Loading branch information
imhappi committed Aug 23, 2023
1 parent faf9a32 commit b6f6eb5
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 3 deletions.
Expand Up @@ -17,7 +17,9 @@
package com.google.android.material.carousel;

import static com.google.android.material.carousel.CarouselStrategyHelper.getExtraSmallSize;
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMin;
import static java.lang.Math.max;
import static java.lang.Math.min;

import android.content.Context;
import androidx.recyclerview.widget.RecyclerView.LayoutParams;
Expand Down Expand Up @@ -71,6 +73,31 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
int largeCount = (int) Math.floor(availableSpace/largeChildSize);
float remainingSpace = availableSpace - largeCount*largeChildSize;
int mediumCount = 0;
boolean isCenter = carousel.getCarouselAlignment() == CarouselLayoutManager.ALIGNMENT_CENTER;

if (isCenter) {
remainingSpace /= 2F;
float smallChildSizeMin = getSmallSizeMin(child.getContext()) + childMargins;
// Ideally we would like to choose a size 3x the remaining space such that 2/3 are cut off.
// If this is bigger than the large child size however, we limit the child size to the large
// child size.
mediumChildSize = min(3*remainingSpace, largeChildSize);

// We also have a minimum child width such that the size is not too small.
mediumChildSize = max(mediumChildSize, smallChildSizeMin);

// Note that a center aligned keyline state will always have exactly 2 mediums with this
// strategy; one to be cut off at the front, and one for the end.
return createCenterAlignedKeylineState(
availableSpace,
childMargins,
largeChildSize,
largeCount,
mediumChildSize,
xSmallChildSize,
remainingSpace);
}

// 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.
Expand All @@ -85,7 +112,7 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
mediumChildSize = max(remainingSpace + remainingSpace/2F, mediumChildSize);
}

return createKeylineState(
return createLeftAlignedKeylineState(
child.getContext(),
childMargins,
availableSpace,
Expand All @@ -96,7 +123,45 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
xSmallChildSize);
}

private KeylineState createKeylineState(
private KeylineState createCenterAlignedKeylineState(
float availableSpace,
float childMargins,
float largeSize,
int largeCount,
float mediumSize,
float xSmallSize,
float remainingSpace) {

float extraSmallMask = getChildMaskPercentage(xSmallSize, largeSize, childMargins);
float mediumMask = getChildMaskPercentage(mediumSize, largeSize, childMargins);
float largeMask = 0F;

float start = 0F;
// Take the remaining space and show as much as you can
float firstMediumCenterX = start + remainingSpace - mediumSize/2F;
start = firstMediumCenterX + mediumSize / 2F;
float extraSmallHeadCenterX = firstMediumCenterX - mediumSize / 2F - (xSmallSize / 2F);

float largeStartCenterX = start + largeSize / 2F;
start += largeCount * largeSize;

KeylineState.Builder builder =
new KeylineState.Builder(largeSize, availableSpace)
.addAnchorKeyline(extraSmallHeadCenterX, extraSmallMask, xSmallSize)
.addKeyline(firstMediumCenterX, mediumMask, mediumSize, false)
.addKeylineRange(largeStartCenterX, largeMask, largeSize, largeCount, true);

float secondMediumCenterX = start + mediumSize / 2F;
start += mediumSize;
builder.addKeyline(
secondMediumCenterX, mediumMask, mediumSize, false);

float xSmallCenterX = start + xSmallSize / 2F;
builder.addAnchorKeyline(xSmallCenterX, extraSmallMask, xSmallSize);
return builder.build();
}

private KeylineState createLeftAlignedKeylineState(
Context context,
float childMargins,
float availableSpace,
Expand Down
Expand Up @@ -196,6 +196,30 @@ public int getCarouselAlignment() {
};
}

static Carousel createCenterAlignedCarouselWithSize(int size) {
return new Carousel() {
@Override
public int getContainerWidth() {
return size;
}

@Override
public int getContainerHeight() {
return size;
}

@Override
public boolean isHorizontal() {
return true;
}

@Override
public int getCarouselAlignment() {
return CarouselLayoutManager.ALIGNMENT_CENTER;
}
};
}

/**
* Creates a {@link Carousel} with a specified {@code size} for both width and height and the
* specified alignment and orientation.
Expand Down
Expand Up @@ -18,7 +18,9 @@
import com.google.android.material.test.R;

import static com.google.android.material.carousel.CarouselHelper.createCarouselWithWidth;
import static com.google.android.material.carousel.CarouselHelper.createCenterAlignedCarouselWithSize;
import static com.google.android.material.carousel.CarouselHelper.createViewWithSize;
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMin;
import static com.google.common.truth.Truth.assertThat;

import android.view.View;
Expand Down Expand Up @@ -48,7 +50,8 @@ public void testLargeItem_withFullCarouselWidth() {
assertThat(keylineState.getKeylines()).hasSize(4);
assertThat(keylineState.getKeylines().get(0).locOffset).isLessThan(0F);
assertThat(keylineState.getKeylines().get(1).mask).isEqualTo(0F);
assertThat(keylineState.getKeylines().get(2).locOffset).isEqualTo(carousel.getContainerWidth() + xSmallSize/2F);
assertThat(keylineState.getKeylines().get(2).locOffset)
.isEqualTo(carousel.getContainerWidth() + xSmallSize / 2F);
assertThat(Iterables.getLast(keylineState.getKeylines()).locOffset)
.isGreaterThan((float) carousel.getContainerWidth());
}
Expand Down Expand Up @@ -119,4 +122,85 @@ public void testRemainingSpaceWithItemSize_fitsLargeItemWithCutoff() {
assertThat(Iterables.getLast(keylineState.getKeylines()).locOffset)
.isGreaterThan((float) carousel.getContainerWidth());
}

@Test
public void testCenterAligned_defaultKeylineHasTwoCutoffs() {
Carousel carousel = createCenterAlignedCarouselWithSize(400);
UncontainedCarouselStrategy config = new UncontainedCarouselStrategy();
int itemSize = 250;
// With this item size, we have 400 - 250 = 150 remaining space which means 75 on each side
// of one focal item.
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), itemSize, 400);

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

// The layout should be [xSmall-medium-large-medium-xSmall]
assertThat(keylineState.getKeylines()).hasSize(5);
assertThat(keylineState.getKeylines().get(0).locOffset).isLessThan(0F);
assertThat(keylineState.getKeylines().get(1).cutoff)
.isEqualTo(150F); // 75*2 since 2/3 should be cut off
assertThat(keylineState.getKeylines().get(2).mask).isEqualTo(0F);
assertThat(keylineState.getKeylines().get(3).cutoff)
.isEqualTo(150F); // 75*2 since 2/3 should be cut off
assertThat(keylineState.getKeylines().get(4).locOffset)
.isGreaterThan((float) carousel.getContainerWidth());
}

@Test
public void testCenterAligned_cutoffMinSize() {
Carousel carousel = createCenterAlignedCarouselWithSize(400);
UncontainedCarouselStrategy config = new UncontainedCarouselStrategy();
int itemSize = 200;
// 2 items fit perfectly in the width so there is no remaining space. Medium items should still
// be the minimum item mask size.
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), itemSize, 400);

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

float minSmallSize = getSmallSizeMin(ApplicationProvider.getApplicationContext());

// The layout should be [xSmall-medium-large-large-medium-xSmall]
assertThat(keylineState.getKeylines()).hasSize(6);
assertThat(keylineState.getKeylines().get(0).locOffset).isLessThan(0F);
assertThat(keylineState.getKeylines().get(1).cutoff)
.isEqualTo(keylineState.getKeylines().get(1).maskedItemSize);
assertThat(keylineState.getKeylines().get(1).locOffset).isLessThan(0F);
assertThat(keylineState.getKeylines().get(1).maskedItemSize).isEqualTo(minSmallSize);
assertThat(keylineState.getKeylines().get(2).mask).isEqualTo(0F);
assertThat(keylineState.getKeylines().get(3).mask).isEqualTo(0F);
assertThat(keylineState.getKeylines().get(4).cutoff)
.isEqualTo(keylineState.getKeylines().get(1).maskedItemSize);
assertThat(keylineState.getKeylines().get(4).locOffset)
.isGreaterThan((float) carousel.getContainerWidth());
assertThat(keylineState.getKeylines().get(4).maskedItemSize).isEqualTo(minSmallSize);
assertThat(keylineState.getKeylines().get(5).locOffset)
.isGreaterThan((float) carousel.getContainerWidth());
}

@Test
public void testCenterAligned_cutoffMaxSize() {
Carousel carousel = createCenterAlignedCarouselWithSize(400);
UncontainedCarouselStrategy config = new UncontainedCarouselStrategy();
int itemSize = 140;
// 2 items fit into width of 400 with 120 remaining space; 60 on each side. Only a 1/3 should be
// showing which means an item width of 180 for the cut off items, but we do not want these
// items to be bigger than the focal item so the max item size should be the focal item size.
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), itemSize, 400);

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

// The layout should be [xSmall-medium-large-large-medium-xSmall]
assertThat(keylineState.getKeylines()).hasSize(6);
assertThat(keylineState.getKeylines().get(0).locOffset).isLessThan(0F);
assertThat(keylineState.getKeylines().get(1).maskedItemSize).isEqualTo((float) itemSize);
// Item size should be max size: 180F, so 140 - 60 (remaining space) = 80
assertThat(keylineState.getKeylines().get(1).cutoff).isEqualTo(80F);
assertThat(keylineState.getKeylines().get(2).mask).isEqualTo(0F);
assertThat(keylineState.getKeylines().get(3).mask).isEqualTo(0F);
assertThat(keylineState.getKeylines().get(4).maskedItemSize).isEqualTo((float) itemSize);
// Item size should be max size: 180F, so 140 - 60 (remaining space) = 80
assertThat(keylineState.getKeylines().get(4).cutoff).isEqualTo(80F);
assertThat(keylineState.getKeylines().get(5).locOffset)
.isGreaterThan((float) carousel.getContainerWidth());
}
}

0 comments on commit b6f6eb5

Please sign in to comment.