Skip to content

Commit 92a5444

Browse files
imhappidsn5ft
authored andcommitted
[Carousel] Add attributes to change small item size
PiperOrigin-RevId: 580249803
1 parent 5055507 commit 92a5444

File tree

10 files changed

+196
-52
lines changed

10 files changed

+196
-52
lines changed

docs/components/Carousel.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,17 +197,19 @@ the carousel size.
197197

198198
Note that in order to use these attributes on the RecyclerView, CarouselLayoutManager must be set through the RecyclerView attribute `app:layoutManager`.
199199

200-
Element | Attribute | Related method(s) | Default value
201-
--------------- | ----------------------- | ---------------------- | -------------
202-
**Orientation** | `android:orientation` | `setOrientation` | `vertical` (if layoutManager has been set through xml)
203-
**Alignment** | `app:carouselAlignment` | `setCarouselAlignment` | `start`
200+
Element | Attribute | Related method(s) | Default value
201+
------------------- |-------------------------|------------------------| -------------
202+
**Orientation** | `android:orientation` | `setOrientation` | `vertical` (if layoutManager has been set through xml)
203+
**Alignment** | `app:carouselAlignment` | `setCarouselAlignment` | `start`
204204

205205
## Customizing carousel
206206

207207
### Item size
208208

209209
The main means of changing the look of carousel is by setting the height of your `RecyclerView` and width of your item's `MaskableFrameLayout`. The width set in the item layout is used by `CarouselLayoutManager` to determine the size items should be when they are fully unmasked. This width needs to be set to a specific dp value and cannot be set to `wrap_content`. `CarouselLayoutManager` tries to then use a size as close to your item layout's specified width as possible but may increase or decrease this size depending on the `RecyclerView`'s available space. This is needed to create a pleasing arrangement of items which fit within the `RecyclerView`'s bounds. Additionally, `CarouselLayoutManager` will only read and use the width set on the first list item. All remaining items will be laid out using this first item's width.
210210

211+
The small item size range may be customized for strategies that have small items by calling `setSmallItemSizeMin`/`setSmallItemSizeMax`. Note that these strategies choose the small item size within the range that alters the fully unmasked item size as little as possible, and may not correspond with the width of the carousel. For strategies that do not use small items, these methods are a no-op.
212+
211213
### Item shape
212214

213215
`MaskableFrameLayout` takes an `app:shapeAppearance` attribute to determine its corner radius. It's recommended to use the `?attr/shapeAppearanceExtraLarge` shape attribute but this can be set to any `ShapeAppearance` theme attribute or style. See [Shape theming](https://github.com/material-components/material-components-android/tree/master/docs/theming/Shape.md) documentation for more details.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ public void setCarouselStrategy(@NonNull CarouselStrategy carouselStrategy) {
242242
@Override
243243
public void onAttachedToWindow(RecyclerView view) {
244244
super.onAttachedToWindow(view);
245+
carouselStrategy.initialize(view.getContext());
245246
refreshKeylineState();
246247
view.addOnLayoutChangeListener(recyclerViewSizeChangeListener);
247248
}

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.android.material.carousel;
1818

19+
import android.content.Context;
1920
import android.view.View;
2021
import androidx.annotation.FloatRange;
2122
import androidx.annotation.NonNull;
@@ -26,6 +27,17 @@
2627
*/
2728
public abstract class CarouselStrategy {
2829

30+
private float smallSizeMin;
31+
32+
private float smallSizeMax;
33+
34+
void initialize(Context context) {
35+
smallSizeMin =
36+
smallSizeMin > 0 ? smallSizeMin : CarouselStrategyHelper.getSmallSizeMin(context);
37+
smallSizeMax =
38+
smallSizeMax > 0 ? smallSizeMax : CarouselStrategyHelper.getSmallSizeMax(context);
39+
}
40+
2941
/**
3042
* Calculates a keyline arrangement and returns a constructed {@link KeylineState}.
3143
*
@@ -139,4 +151,45 @@ boolean shouldRefreshKeylineState(Carousel carousel, int oldItemCount) {
139151
// state based on item count.
140152
return false;
141153
}
154+
155+
/**
156+
* Sets the minimum size for the small items.
157+
*
158+
* <p> This method is a no-op for strategies that do not have small items.
159+
*
160+
* <p> Note that setting this size may impact other sizes in the carousel
161+
* in order to fit the carousel strategy configuration.
162+
* @param minSmallItemSize size to set the small item to.
163+
*/
164+
public void setSmallItemSizeMin(float minSmallItemSize) {
165+
smallSizeMin = minSmallItemSize;
166+
}
167+
168+
/**
169+
* Sets the maximum size for the small items.
170+
*
171+
* <p> This method is a no-op for strategies that do not have small items.
172+
*
173+
* <p> Note that setting this size may impact other sizes in the carousel
174+
* in order to fit the carousel strategy configuration.
175+
* @param maxSmallItemSize size to set the small item to.
176+
*/
177+
public void setSmallItemSizeMax(float maxSmallItemSize) {
178+
smallSizeMax = maxSmallItemSize;
179+
}
180+
181+
/**
182+
* Returns the minimum small item size value.
183+
*/
184+
public float getSmallItemSizeMin() {
185+
return smallSizeMin;
186+
}
187+
188+
189+
/**
190+
* Returns the maximum small item size value.
191+
*/
192+
public float getSmallItemSizeMax() {
193+
return smallSizeMax;
194+
}
142195
}

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

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

1919
import static com.google.android.material.carousel.CarouselStrategyHelper.createKeylineState;
20-
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMax;
21-
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMin;
2220
import static com.google.android.material.carousel.CarouselStrategyHelper.maxValue;
2321
import static java.lang.Math.ceil;
2422
import static java.lang.Math.floor;
@@ -72,8 +70,10 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
7270
measuredChildSize = child.getMeasuredHeight() * 2;
7371
}
7472

75-
float smallChildSizeMin = getSmallSizeMin(child.getContext()) + childMargins;
76-
float smallChildSizeMax = getSmallSizeMax(child.getContext()) + childMargins;
73+
float smallChildSizeMin = getSmallItemSizeMin() + childMargins;
74+
float smallChildSizeMax = getSmallItemSizeMax() + childMargins;
75+
// Ensure that the max size at least as big as the small size.
76+
smallChildSizeMax = max(smallChildSizeMax, smallChildSizeMin);
7777

7878
float targetLargeChildSize = min(measuredChildSize + childMargins, availableSpace);
7979
// Ideally we would like to create a balanced arrangement where a small item is 1/3 the size of
@@ -82,8 +82,8 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
8282
float targetSmallChildSize =
8383
MathUtils.clamp(
8484
measuredChildSize / 3F + childMargins,
85-
getSmallSizeMin(child.getContext()) + childMargins,
86-
getSmallSizeMax(child.getContext()) + childMargins);
85+
smallChildSizeMin + childMargins,
86+
smallChildSizeMax + childMargins);
8787
float targetMediumChildSize = (targetLargeChildSize + targetSmallChildSize) / 2F;
8888

8989
int[] smallCounts = SMALL_COUNTS;

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
package com.google.android.material.carousel;
1818

1919
import static com.google.android.material.carousel.CarouselStrategyHelper.createKeylineState;
20-
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMax;
21-
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMin;
2220
import static com.google.android.material.carousel.CarouselStrategyHelper.maxValue;
2321
import static java.lang.Math.ceil;
2422
import static java.lang.Math.floor;
@@ -74,8 +72,9 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
7472
measuredChildSize = child.getMeasuredWidth();
7573
}
7674

77-
float smallChildSizeMin = getSmallSizeMin(child.getContext()) + childMargins;
78-
float smallChildSizeMax = getSmallSizeMax(child.getContext()) + childMargins;
75+
float smallChildSizeMin = getSmallItemSizeMin() + childMargins;
76+
float smallChildSizeMax = getSmallItemSizeMax() + childMargins;
77+
smallChildSizeMax = max(smallChildSizeMax, smallChildSizeMin);
7978

8079
float targetLargeChildSize = min(measuredChildSize + childMargins, availableSpace);
8180
// Ideally we would like to create a balanced arrangement where a small item is 1/3 the size of
@@ -85,8 +84,8 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
8584
float targetSmallChildSize =
8685
MathUtils.clamp(
8786
measuredChildSize / 3F + childMargins,
88-
getSmallSizeMin(child.getContext()) + childMargins,
89-
getSmallSizeMax(child.getContext()) + childMargins);
87+
smallChildSizeMin + childMargins,
88+
smallChildSizeMax + childMargins);
9089
float targetMediumChildSize = (targetLargeChildSize + targetSmallChildSize) / 2F;
9190

9291
// Create arrays representing the possible count of small, medium, and large items. These are

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
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;
2120
import static java.lang.Math.max;
2221
import static java.lang.Math.min;
2322

@@ -78,7 +77,7 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
7877

7978
if (isCenter) {
8079
remainingSpace /= 2F;
81-
float smallChildSizeMin = getSmallSizeMin(child.getContext()) + childMargins;
80+
float smallChildSizeMin = getSmallItemSizeMin() + childMargins;
8281
// Ideally we would like to choose a size 3x the remaining space such that 2/3 are cut off.
8382
// If this is bigger than the large child size however, we limit the child size to the large
8483
// child size.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,17 +236,17 @@ public int getItemCount() {
236236
* Creates a {@link Carousel} with a specified {@code size} for both width and height and the
237237
* specified alignment and orientation.
238238
*/
239-
static Carousel createCarousel(int size, int orientation, int alignment) {
239+
static Carousel createCarousel(int width, int height, int orientation, int alignment) {
240240
return new Carousel() {
241241

242242
@Override
243243
public int getContainerWidth() {
244-
return size;
244+
return width;
245245
}
246246

247247
@Override
248248
public int getContainerHeight() {
249-
return size;
249+
return height;
250250
}
251251

252252
@Override

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

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class HeroCarouselStrategyTest {
4040
@Test
4141
public void testItemSameAsContainerSize_showsOneLargeOneSmall() {
4242
Carousel carousel = createCarouselWithWidth(400);
43-
HeroCarouselStrategy config = new HeroCarouselStrategy();
43+
HeroCarouselStrategy config = setupStrategy();
4444
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 400, 400);
4545

4646
KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view);
@@ -61,7 +61,7 @@ public void testItemSameAsContainerSize_showsOneLargeOneSmall() {
6161
@Test
6262
public void testItemSmallerThanContainer_showsOneLargeOneSmall() {
6363
Carousel carousel = createCarouselWithWidth(400);
64-
HeroCarouselStrategy config = new HeroCarouselStrategy();
64+
HeroCarouselStrategy config = setupStrategy();
6565
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 100, 400);
6666

6767
KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view);
@@ -89,7 +89,7 @@ public void testSmallContainer_shouldShowOneLargeItem() {
8989
int carouselWidth = (int) (minSmallItemSize * 1.5f);
9090
Carousel carousel = createCarouselWithWidth(carouselWidth);
9191

92-
HeroCarouselStrategy config = new HeroCarouselStrategy();
92+
HeroCarouselStrategy config = setupStrategy();
9393
KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view);
9494

9595
assertThat(keylineState.getKeylines()).hasSize(3);
@@ -100,7 +100,7 @@ public void testSmallContainer_shouldShowOneLargeItem() {
100100
public void testKnownArrangement_correctlyCalculatesKeylineLocations() {
101101
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 400, 200);
102102

103-
HeroCarouselStrategy config = new HeroCarouselStrategy();
103+
HeroCarouselStrategy config = setupStrategy();
104104
float extraSmallSize =
105105
view.getResources().getDimension(R.dimen.m3_carousel_gone_size);
106106
float minSmallItemSize =
@@ -132,7 +132,7 @@ public void testKnownArrangementWithMargins_correctlyCalculatesKeylineLocations(
132132
layoutParams.leftMargin += 50;
133133
layoutParams.rightMargin += 30;
134134

135-
HeroCarouselStrategy config = new HeroCarouselStrategy();
135+
HeroCarouselStrategy config = setupStrategy();
136136
float extraSmallSize =
137137
view.getResources().getDimension(R.dimen.m3_carousel_gone_size);
138138
float minSmallItemSize =
@@ -167,13 +167,17 @@ public void testKnownCenterAlignmentArrangement_correctlyCalculatesKeylineLocati
167167
ApplicationProvider.getApplicationContext(), (int) largeSize, (int) largeSize);
168168
int carouselSize = (int) (largeSize + smallSize * 2);
169169

170-
HeroCarouselStrategy strategy = new HeroCarouselStrategy();
170+
HeroCarouselStrategy strategy = setupStrategy();
171171
List<Keyline> keylines =
172-
strategy.onFirstChildMeasuredWithMargins(
173-
createCarousel(
174-
carouselSize,
175-
CarouselLayoutManager.HORIZONTAL,
176-
CarouselLayoutManager.ALIGNMENT_CENTER), view).getKeylines();
172+
strategy
173+
.onFirstChildMeasuredWithMargins(
174+
createCarousel(
175+
carouselSize,
176+
carouselSize,
177+
CarouselLayoutManager.HORIZONTAL,
178+
CarouselLayoutManager.ALIGNMENT_CENTER),
179+
view)
180+
.getKeylines();
177181

178182
float[] locOffsets = new float[] {-.5F, 20F, 100F, 180F, 200.5F};
179183

@@ -192,7 +196,7 @@ public void testCenterAlignment_isLeftAlignedWithMinItems() {
192196
ApplicationProvider.getApplicationContext(), (int) largeSize, (int) largeSize);
193197
int carouselSize = (int) (largeSize + smallSize * 2);
194198

195-
HeroCarouselStrategy strategy = new HeroCarouselStrategy();
199+
HeroCarouselStrategy strategy = setupStrategy();
196200
List<Keyline> keylines =
197201
strategy
198202
.onFirstChildMeasuredWithMargins(
@@ -212,4 +216,53 @@ public void testCenterAlignment_isLeftAlignedWithMinItems() {
212216
assertThat(keylines.get(i).locOffset).isEqualTo(locOffsets[i]);
213217
}
214218
}
219+
220+
@Test
221+
public void testSettingSmallRange_setsToMinSize() {
222+
Carousel carousel = createCarouselWithWidth(400);
223+
HeroCarouselStrategy config = setupStrategy();
224+
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 400, 400);
225+
226+
float minSmallItemSize = 20;
227+
config.setSmallItemSizeMin(minSmallItemSize);
228+
config.setSmallItemSizeMax(1234);
229+
KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view);
230+
231+
// A fullscreen layout should be [xSmall-large-small-xSmall] where the xSmall items are
232+
// outside the bounds of the carousel container and the large center item takes up the
233+
// containers full width.
234+
assertThat(keylineState.getKeylines()).hasSize(4);
235+
assertThat(keylineState.getKeylines().get(0).locOffset).isLessThan(0F);
236+
assertThat(Iterables.getLast(keylineState.getKeylines()).locOffset)
237+
.isGreaterThan((float) carousel.getContainerWidth());
238+
assertThat(keylineState.getKeylines().get(1).mask).isEqualTo(0F);
239+
assertThat(keylineState.getKeylines().get(2).maskedItemSize).isEqualTo(minSmallItemSize);
240+
}
241+
242+
@Test
243+
public void testLargeCarouselWidth_correctlyCalculatesKeylineLocations() {
244+
int width = 400;
245+
int height = 100;
246+
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), width, height);
247+
248+
HeroCarouselStrategy config = setupStrategy();
249+
250+
List<Keyline> keylines =
251+
config
252+
.onFirstChildMeasuredWithMargins(
253+
createCarousel(
254+
width,
255+
height,
256+
CarouselLayoutManager.HORIZONTAL,
257+
CarouselLayoutManager.ALIGNMENT_START),
258+
view)
259+
.getKeylines();
260+
assertThat(keylines.get(1).maskedItemSize).isEqualTo(height * 2f);
261+
}
262+
263+
private HeroCarouselStrategy setupStrategy() {
264+
HeroCarouselStrategy strategy = new HeroCarouselStrategy();
265+
strategy.initialize(ApplicationProvider.getApplicationContext());
266+
return strategy;
267+
}
215268
}

0 commit comments

Comments
 (0)