Skip to content

Commit b6f6eb5

Browse files
committed
[Carousel] Center aligned uncontained carousel
PiperOrigin-RevId: 559215330
1 parent faf9a32 commit b6f6eb5

File tree

3 files changed

+176
-3
lines changed

3 files changed

+176
-3
lines changed

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

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
package com.google.android.material.carousel;
1818

1919
import static com.google.android.material.carousel.CarouselStrategyHelper.getExtraSmallSize;
20+
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMin;
2021
import static java.lang.Math.max;
22+
import static java.lang.Math.min;
2123

2224
import android.content.Context;
2325
import androidx.recyclerview.widget.RecyclerView.LayoutParams;
@@ -71,6 +73,31 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
7173
int largeCount = (int) Math.floor(availableSpace/largeChildSize);
7274
float remainingSpace = availableSpace - largeCount*largeChildSize;
7375
int mediumCount = 0;
76+
boolean isCenter = carousel.getCarouselAlignment() == CarouselLayoutManager.ALIGNMENT_CENTER;
77+
78+
if (isCenter) {
79+
remainingSpace /= 2F;
80+
float smallChildSizeMin = getSmallSizeMin(child.getContext()) + childMargins;
81+
// Ideally we would like to choose a size 3x the remaining space such that 2/3 are cut off.
82+
// If this is bigger than the large child size however, we limit the child size to the large
83+
// child size.
84+
mediumChildSize = min(3*remainingSpace, largeChildSize);
85+
86+
// We also have a minimum child width such that the size is not too small.
87+
mediumChildSize = max(mediumChildSize, smallChildSizeMin);
88+
89+
// Note that a center aligned keyline state will always have exactly 2 mediums with this
90+
// strategy; one to be cut off at the front, and one for the end.
91+
return createCenterAlignedKeylineState(
92+
availableSpace,
93+
childMargins,
94+
largeChildSize,
95+
largeCount,
96+
mediumChildSize,
97+
xSmallChildSize,
98+
remainingSpace);
99+
}
100+
74101
// If the keyline location for the next large size would be within the remaining space,
75102
// then we can place a large child there as the last non-anchor keyline because visually
76103
// keylines will become smaller as it goes past the large keyline location.
@@ -85,7 +112,7 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
85112
mediumChildSize = max(remainingSpace + remainingSpace/2F, mediumChildSize);
86113
}
87114

88-
return createKeylineState(
115+
return createLeftAlignedKeylineState(
89116
child.getContext(),
90117
childMargins,
91118
availableSpace,
@@ -96,7 +123,45 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
96123
xSmallChildSize);
97124
}
98125

99-
private KeylineState createKeylineState(
126+
private KeylineState createCenterAlignedKeylineState(
127+
float availableSpace,
128+
float childMargins,
129+
float largeSize,
130+
int largeCount,
131+
float mediumSize,
132+
float xSmallSize,
133+
float remainingSpace) {
134+
135+
float extraSmallMask = getChildMaskPercentage(xSmallSize, largeSize, childMargins);
136+
float mediumMask = getChildMaskPercentage(mediumSize, largeSize, childMargins);
137+
float largeMask = 0F;
138+
139+
float start = 0F;
140+
// Take the remaining space and show as much as you can
141+
float firstMediumCenterX = start + remainingSpace - mediumSize/2F;
142+
start = firstMediumCenterX + mediumSize / 2F;
143+
float extraSmallHeadCenterX = firstMediumCenterX - mediumSize / 2F - (xSmallSize / 2F);
144+
145+
float largeStartCenterX = start + largeSize / 2F;
146+
start += largeCount * largeSize;
147+
148+
KeylineState.Builder builder =
149+
new KeylineState.Builder(largeSize, availableSpace)
150+
.addAnchorKeyline(extraSmallHeadCenterX, extraSmallMask, xSmallSize)
151+
.addKeyline(firstMediumCenterX, mediumMask, mediumSize, false)
152+
.addKeylineRange(largeStartCenterX, largeMask, largeSize, largeCount, true);
153+
154+
float secondMediumCenterX = start + mediumSize / 2F;
155+
start += mediumSize;
156+
builder.addKeyline(
157+
secondMediumCenterX, mediumMask, mediumSize, false);
158+
159+
float xSmallCenterX = start + xSmallSize / 2F;
160+
builder.addAnchorKeyline(xSmallCenterX, extraSmallMask, xSmallSize);
161+
return builder.build();
162+
}
163+
164+
private KeylineState createLeftAlignedKeylineState(
100165
Context context,
101166
float childMargins,
102167
float availableSpace,

lib/javatests/com/google/android/material/carousel/CarouselHelper.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,30 @@ public int getCarouselAlignment() {
196196
};
197197
}
198198

199+
static Carousel createCenterAlignedCarouselWithSize(int size) {
200+
return new Carousel() {
201+
@Override
202+
public int getContainerWidth() {
203+
return size;
204+
}
205+
206+
@Override
207+
public int getContainerHeight() {
208+
return size;
209+
}
210+
211+
@Override
212+
public boolean isHorizontal() {
213+
return true;
214+
}
215+
216+
@Override
217+
public int getCarouselAlignment() {
218+
return CarouselLayoutManager.ALIGNMENT_CENTER;
219+
}
220+
};
221+
}
222+
199223
/**
200224
* Creates a {@link Carousel} with a specified {@code size} for both width and height and the
201225
* specified alignment and orientation.

lib/javatests/com/google/android/material/carousel/UncontainedCarouselStrategyTest.java

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import com.google.android.material.test.R;
1919

2020
import static com.google.android.material.carousel.CarouselHelper.createCarouselWithWidth;
21+
import static com.google.android.material.carousel.CarouselHelper.createCenterAlignedCarouselWithSize;
2122
import static com.google.android.material.carousel.CarouselHelper.createViewWithSize;
23+
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMin;
2224
import static com.google.common.truth.Truth.assertThat;
2325

2426
import android.view.View;
@@ -48,7 +50,8 @@ public void testLargeItem_withFullCarouselWidth() {
4850
assertThat(keylineState.getKeylines()).hasSize(4);
4951
assertThat(keylineState.getKeylines().get(0).locOffset).isLessThan(0F);
5052
assertThat(keylineState.getKeylines().get(1).mask).isEqualTo(0F);
51-
assertThat(keylineState.getKeylines().get(2).locOffset).isEqualTo(carousel.getContainerWidth() + xSmallSize/2F);
53+
assertThat(keylineState.getKeylines().get(2).locOffset)
54+
.isEqualTo(carousel.getContainerWidth() + xSmallSize / 2F);
5255
assertThat(Iterables.getLast(keylineState.getKeylines()).locOffset)
5356
.isGreaterThan((float) carousel.getContainerWidth());
5457
}
@@ -119,4 +122,85 @@ public void testRemainingSpaceWithItemSize_fitsLargeItemWithCutoff() {
119122
assertThat(Iterables.getLast(keylineState.getKeylines()).locOffset)
120123
.isGreaterThan((float) carousel.getContainerWidth());
121124
}
125+
126+
@Test
127+
public void testCenterAligned_defaultKeylineHasTwoCutoffs() {
128+
Carousel carousel = createCenterAlignedCarouselWithSize(400);
129+
UncontainedCarouselStrategy config = new UncontainedCarouselStrategy();
130+
int itemSize = 250;
131+
// With this item size, we have 400 - 250 = 150 remaining space which means 75 on each side
132+
// of one focal item.
133+
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), itemSize, 400);
134+
135+
KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view);
136+
137+
// The layout should be [xSmall-medium-large-medium-xSmall]
138+
assertThat(keylineState.getKeylines()).hasSize(5);
139+
assertThat(keylineState.getKeylines().get(0).locOffset).isLessThan(0F);
140+
assertThat(keylineState.getKeylines().get(1).cutoff)
141+
.isEqualTo(150F); // 75*2 since 2/3 should be cut off
142+
assertThat(keylineState.getKeylines().get(2).mask).isEqualTo(0F);
143+
assertThat(keylineState.getKeylines().get(3).cutoff)
144+
.isEqualTo(150F); // 75*2 since 2/3 should be cut off
145+
assertThat(keylineState.getKeylines().get(4).locOffset)
146+
.isGreaterThan((float) carousel.getContainerWidth());
147+
}
148+
149+
@Test
150+
public void testCenterAligned_cutoffMinSize() {
151+
Carousel carousel = createCenterAlignedCarouselWithSize(400);
152+
UncontainedCarouselStrategy config = new UncontainedCarouselStrategy();
153+
int itemSize = 200;
154+
// 2 items fit perfectly in the width so there is no remaining space. Medium items should still
155+
// be the minimum item mask size.
156+
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), itemSize, 400);
157+
158+
KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view);
159+
160+
float minSmallSize = getSmallSizeMin(ApplicationProvider.getApplicationContext());
161+
162+
// The layout should be [xSmall-medium-large-large-medium-xSmall]
163+
assertThat(keylineState.getKeylines()).hasSize(6);
164+
assertThat(keylineState.getKeylines().get(0).locOffset).isLessThan(0F);
165+
assertThat(keylineState.getKeylines().get(1).cutoff)
166+
.isEqualTo(keylineState.getKeylines().get(1).maskedItemSize);
167+
assertThat(keylineState.getKeylines().get(1).locOffset).isLessThan(0F);
168+
assertThat(keylineState.getKeylines().get(1).maskedItemSize).isEqualTo(minSmallSize);
169+
assertThat(keylineState.getKeylines().get(2).mask).isEqualTo(0F);
170+
assertThat(keylineState.getKeylines().get(3).mask).isEqualTo(0F);
171+
assertThat(keylineState.getKeylines().get(4).cutoff)
172+
.isEqualTo(keylineState.getKeylines().get(1).maskedItemSize);
173+
assertThat(keylineState.getKeylines().get(4).locOffset)
174+
.isGreaterThan((float) carousel.getContainerWidth());
175+
assertThat(keylineState.getKeylines().get(4).maskedItemSize).isEqualTo(minSmallSize);
176+
assertThat(keylineState.getKeylines().get(5).locOffset)
177+
.isGreaterThan((float) carousel.getContainerWidth());
178+
}
179+
180+
@Test
181+
public void testCenterAligned_cutoffMaxSize() {
182+
Carousel carousel = createCenterAlignedCarouselWithSize(400);
183+
UncontainedCarouselStrategy config = new UncontainedCarouselStrategy();
184+
int itemSize = 140;
185+
// 2 items fit into width of 400 with 120 remaining space; 60 on each side. Only a 1/3 should be
186+
// showing which means an item width of 180 for the cut off items, but we do not want these
187+
// items to be bigger than the focal item so the max item size should be the focal item size.
188+
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), itemSize, 400);
189+
190+
KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view);
191+
192+
// The layout should be [xSmall-medium-large-large-medium-xSmall]
193+
assertThat(keylineState.getKeylines()).hasSize(6);
194+
assertThat(keylineState.getKeylines().get(0).locOffset).isLessThan(0F);
195+
assertThat(keylineState.getKeylines().get(1).maskedItemSize).isEqualTo((float) itemSize);
196+
// Item size should be max size: 180F, so 140 - 60 (remaining space) = 80
197+
assertThat(keylineState.getKeylines().get(1).cutoff).isEqualTo(80F);
198+
assertThat(keylineState.getKeylines().get(2).mask).isEqualTo(0F);
199+
assertThat(keylineState.getKeylines().get(3).mask).isEqualTo(0F);
200+
assertThat(keylineState.getKeylines().get(4).maskedItemSize).isEqualTo((float) itemSize);
201+
// Item size should be max size: 180F, so 140 - 60 (remaining space) = 80
202+
assertThat(keylineState.getKeylines().get(4).cutoff).isEqualTo(80F);
203+
assertThat(keylineState.getKeylines().get(5).locOffset)
204+
.isGreaterThan((float) carousel.getContainerWidth());
205+
}
122206
}

0 commit comments

Comments
 (0)