Skip to content

Commit 74b5900

Browse files
imhappidsn5ft
authored andcommitted
[Carousel] Make carousel internal classes public to enable custom strategies
PiperOrigin-RevId: 679710469
1 parent 8c4da47 commit 74b5900

File tree

14 files changed

+146
-40
lines changed

14 files changed

+146
-40
lines changed

docs/components/Carousel.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,4 +260,98 @@ carouselLayoutManager.setCarouselAlignment(CarouselLayoutManager.CENTER)
260260

261261
By default, the focal alignment is start-aligned.
262262

263+
### Creating a custom strategy
264+
265+
You can create a custom Carousel strategy by extending the `CarouselStrategy`
266+
class, and overriding the `onFirstChildMeasuredWithMargins` method. This method
267+
is called when the RecyclerView measures the first item to be added to its
268+
scroll container. This method must return a `KeylineState` which tells the
269+
Carousel how to fill the scroll container with items - how many are visible at
270+
once, what their sizes are, and where they're placed.
271+
272+
This is done by using the `KeylineState.Builder` to add "keylines", which
273+
represent items that will be shown. For example, if there are 3 non-anchor
274+
keylines, 3 items will be shown in the carousel. As the items move through the
275+
carousel, each item will be the exact size specified by each keyline when they
276+
are directly at the center of the keyline.
277+
278+
Note that anchor keylines are used to control how small items become as they
279+
reach the edge of the carousel, and must always be added to denote the start and
280+
ends of the keyline strategy. Smaller anchor keyline sizes will result in items
281+
looking 'squeezed' to the edges of the carousel, whereas bigger anchor sizes
282+
will make it look like there's no size change as it goes off-screen.
283+
284+
Keylines must always follow rule patterns:
285+
286+
- Anchor keylines must always be smaller than any other keyline (it wouldn't
287+
make sense for an item to get bigger as it goes off-screen)
288+
- Anchor keylines must always be off-screen; typically, the left anchor's
289+
center is located at `-(anchorSize)/2` and the right anchor's center is
290+
located at `availableSpace + anchorSize/2`
291+
- The biggest keyline must be focal keylines; these are items that are fully
292+
unmasked
293+
- Focal keylines must be grouped together, and there must be at least one
294+
focal keyline
295+
- Non-anchor keylines must be in ascending size order up to the focal
296+
keyline(s), and in descending order after the focal keyline(s)
297+
- All non-anchor keyline sizes must add up to the available space in the
298+
carousel
299+
300+
For example, if you want the following strategy structure:
301+
302+
![A custom Carousel](assets/carousel/custom.png)
303+
304+
Then you can define it with a `KeylineState` like below in your custom Strategy.
305+
The `KeylineState` returned by `onFirstChildMeasuredWithMargins` should always
306+
be the 'default' `KeylineState`, meaning the `KeylineState` with no altered
307+
behavior. The `CarouselLayoutManager` will take care of special behavior at the
308+
front and ends of the RecyclerView by taking the default `KeylineState` given
309+
and altering them.
263310

311+
```kt
312+
override fun onFirstChildMeasuredWithMargins(
313+
carousel: Carousel,
314+
child: View,
315+
): KeylineState? {
316+
val availableSpace = if (carousel.isHorizontal()) carousel.containerWidth else carousel.containerHeight
317+
val focalItemSize = 200f
318+
val mediumItemSize = 160f
319+
val smallItemSize = 80f
320+
val anchorSize = 50f // the anchor size determines how small items become as they reach the edge of the carousel
321+
val childLayoutParams = child.layoutParams as LayoutParams
322+
val childMargins = (childLayoutParams.leftMargin + childLayoutParams.rightMargin).toFloat() // this will be top and bottom margins for vertical strategies
323+
val anchorMaskSize = CarouselStrategy.getChildMaskPercentage(
324+
anchorSize,
325+
focalItemSize,
326+
childMargins
327+
)
328+
val mediumMaskSize = CarouselStrategy.getChildMaskPercentage(
329+
mediumItemSize,
330+
focalItemSize,
331+
childMargins
332+
)
333+
val smallMaskSize = CarouselStrategy.getChildMaskPercentage(
334+
smallItemSize,
335+
focalItemSize,
336+
childMargins
337+
)
338+
val leftAnchorCenterX = -anchorSize / 2f // the anchor keyline
339+
val rightAnchorCenterX = availableSpace + anchorSize / 2f
340+
return
341+
KeylineState.Builder(/* itemSize= */ focalItemSize, /* availableSpace= */ availableSpace)
342+
.addAnchorKeyline(/* offsetLoc= */ leftAnchorCenterX, /* mask= */ anchorMaskSize, /* maskedItemSize= */ anchorSize)
343+
.addKeyline(/* offsetLoc= */ focalItemSize / 2f, /* mask= */ 0, /* maskedItemSize= */ focalItemSize, /* isFocal= */ true)
344+
.addKeyline(/* offsetLoc= */ focalItemSize + mediumItemSize / 2f, /* mask= */ mediumMaskSize, /* maskedItemSize= */ mediumItemSize)
345+
.addKeyline(/* offsetLoc= */ focalItemSize + mediumItemSize + smallItemSize / 2f, /* mask= */ smallMaskSize, /* maskedItemSize= * smallItemSize)
346+
.addAnchorKeyline(/* offsetLoc= */ rightAnchorCenterX, /* mask= */ anchorMaskSize, /* maskedItemSize= */ anchorSize)
347+
.build()
348+
}
349+
```
350+
351+
You may also use the helper class `Arrangement` and the helper method
352+
`Arrangement.findLowestCostArrangement` to calculate the best suited arrangement
353+
for the given available space, and the given desired number of items along with
354+
their desired sizes. `Arrangement.findLowestCostArrangement` will give you an
355+
arrangement of items and sizes that will fit within the available space. This is
356+
encouraged unless you're doing some custom calculations with the available
357+
carousel size to determine an exact size for all of the keylines.
8.25 KB
Loading

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@
2020
import static java.lang.Math.min;
2121

2222
import androidx.annotation.NonNull;
23+
import androidx.annotation.Nullable;
2324
import androidx.core.math.MathUtils;
2425

2526
/**
2627
* A class that holds data about a combination of large, medium, and small items, knows how to alter
2728
* an arrangement to fit within an available space, and can assess the arrangement's
2829
* desirability according to a priority heuristic.
2930
*/
30-
final class Arrangement {
31+
public final class Arrangement {
3132

3233
// Specifies a percentage of a medium item's size by which it can be increased or decreased to
3334
// help fit an arrangement into the carousel's available space.
@@ -64,7 +65,7 @@ final class Arrangement {
6465
* @param largeCount the number of large items in this arrangement
6566
* @param availableSpace the space this arrangement needs to fit within
6667
*/
67-
Arrangement(
68+
public Arrangement(
6869
int priority,
6970
float targetSmallSize,
7071
float minSmallSize,
@@ -237,16 +238,17 @@ private float cost(float targetLargeSize) {
237238
* @return the arrangement that is considered the most desirable and has been adjusted to fit
238239
* within the available space
239240
*/
240-
static Arrangement findLowestCostArrangement(
241+
@Nullable
242+
public static Arrangement findLowestCostArrangement(
241243
float availableSpace,
242244
float targetSmallSize,
243245
float minSmallSize,
244246
float maxSmallSize,
245-
int[] smallCounts,
247+
@NonNull int[] smallCounts,
246248
float targetMediumSize,
247-
int[] mediumCounts,
249+
@NonNull int[] mediumCounts,
248250
float targetLargeSize,
249-
int[] largeCounts) {
251+
@NonNull int[] largeCounts) {
250252
Arrangement lowestCostArrangement = null;
251253
int priority = 1;
252254
for (int largeCount : largeCounts) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import com.google.android.material.carousel.CarouselLayoutManager.Alignment;
2020

2121
/** An interface that defines a widget that can be configured as a Carousel. */
22-
interface Carousel {
22+
public interface Carousel {
2323

2424
/** Gets the width of the carousel container. */
2525
int getContainerWidth();

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ void initialize(Context context) {
103103
* @return A {@link KeylineState} to be used by the layout manager to offset and mask children
104104
* along the scrolling axis.
105105
*/
106-
abstract KeylineState onFirstChildMeasuredWithMargins(
106+
@NonNull
107+
public abstract KeylineState onFirstChildMeasuredWithMargins(
107108
@NonNull Carousel carousel, @NonNull View child);
108109

109110
/**
@@ -120,7 +121,8 @@ abstract KeylineState onFirstChildMeasuredWithMargins(
120121
* maskedSize}. 0F is fully unmasked and 1F is fully masked.
121122
*/
122123
@FloatRange(from = 0F, to = 1F)
123-
static float getChildMaskPercentage(float maskedSize, float unmaskedSize, float childMargins) {
124+
public static float getChildMaskPercentage(
125+
float maskedSize, float unmaskedSize, float childMargins) {
124126
return 1F - ((maskedSize - childMargins) / (unmaskedSize - childMargins));
125127
}
126128

@@ -153,11 +155,13 @@ StrategyType getStrategyType() {
153155

154156
/**
155157
* Whether or not the strategy keylines should be refreshed based on the old item count and the
156-
* carousel's current parameters.
158+
* carousel's current parameters. This method is called when the item count is updated, and is
159+
* used to update the keyline strategy when the item count is less than the number of keylines in
160+
* the normal keyline strategy.
157161
*
158162
* @return true if the keylines should be refreshed.
159163
*/
160-
boolean shouldRefreshKeylineState(Carousel carousel, int oldItemCount) {
164+
public boolean shouldRefreshKeylineState(@NonNull Carousel carousel, int oldItemCount) {
161165
// TODO: b/301332183 - Update existing strategies with logic on when to refresh keyline
162166
// state based on item count.
163167
return false;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public class FullScreenCarouselStrategy extends CarouselStrategy {
4343

4444
@Override
4545
@NonNull
46-
KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNull View child) {
46+
public KeylineState onFirstChildMeasuredWithMargins(
47+
@NonNull Carousel carousel, @NonNull View child) {
4748
float availableSpace;
4849
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
4950
float childMargins;

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ public class HeroCarouselStrategy extends CarouselStrategy {
5454

5555
@Override
5656
@NonNull
57-
KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNull View child) {
57+
public KeylineState onFirstChildMeasuredWithMargins(
58+
@NonNull Carousel carousel, @NonNull View child) {
5859
int availableSpace = carousel.getContainerHeight();
5960
if (carousel.isHorizontal()) {
6061
availableSpace = carousel.getContainerWidth();
@@ -147,7 +148,7 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
147148
}
148149

149150
@Override
150-
boolean shouldRefreshKeylineState(@NonNull Carousel carousel, int oldItemCount) {
151+
public boolean shouldRefreshKeylineState(@NonNull Carousel carousel, int oldItemCount) {
151152
return carousel.getCarouselAlignment() == CarouselLayoutManager.ALIGNMENT_CENTER
152153
&& ((oldItemCount < keylineCount && carousel.getItemCount() >= keylineCount)
153154
|| (oldItemCount >= keylineCount && carousel.getItemCount() < keylineCount));

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
* focal keylines at the beginning of the scroll container, center-aligned strategies at the center
5050
* of the scroll container, etc.
5151
*/
52-
final class KeylineState {
52+
public final class KeylineState {
5353

5454
private final float itemSize;
5555
private int totalVisibleFocalItems;
@@ -252,7 +252,7 @@ static KeylineState reverse(KeylineState keylineState, float availableSpace) {
252252
*
253253
* Typically there should be a keyline for every visible item in the scrolling container.
254254
*/
255-
static final class Builder {
255+
public static final class Builder {
256256

257257
private static final int NO_INDEX = -1;
258258
private static final float UNKNOWN_LOC = Float.MIN_VALUE;
@@ -276,7 +276,7 @@ static final class Builder {
276276
/**
277277
* Creates a new {@link KeylineState.Builder}.
278278
*
279-
* @param itemSize The size of a fully unmaksed item. This is the size that will be used by the
279+
* @param itemSize The size of a fully unmasked item. This is the size that will be used by the
280280
* carousel to measure and lay out all children, overriding each child's desired size.
281281
* @param availableSpace The available space of the carousel the keylines calculate cutoffs by.
282282
*/
@@ -305,7 +305,7 @@ static final class Builder {
305305
*/
306306
@NonNull
307307
@CanIgnoreReturnValue
308-
Builder addKeyline(
308+
public Builder addKeyline(
309309
float offsetLoc,
310310
@FloatRange(from = 0.0F, to = 1.0F) float mask,
311311
float maskedItemSize,
@@ -321,7 +321,7 @@ Builder addKeyline(
321321
*/
322322
@NonNull
323323
@CanIgnoreReturnValue
324-
Builder addKeyline(
324+
public Builder addKeyline(
325325
float offsetLoc, @FloatRange(from = 0.0F, to = 1.0F) float mask, float maskedItemSize) {
326326
return addKeyline(offsetLoc, mask, maskedItemSize, false);
327327
}
@@ -353,7 +353,7 @@ Builder addKeyline(
353353
*/
354354
@NonNull
355355
@CanIgnoreReturnValue
356-
Builder addKeyline(
356+
public Builder addKeyline(
357357
float offsetLoc,
358358
@FloatRange(from = 0.0F, to = 1.0F) float mask,
359359
float maskedItemSize,
@@ -440,7 +440,7 @@ Builder addKeyline(
440440
*/
441441
@NonNull
442442
@CanIgnoreReturnValue
443-
Builder addKeyline(
443+
public Builder addKeyline(
444444
float offsetLoc,
445445
@FloatRange(from = 0.0F, to = 1.0F) float mask,
446446
float maskedItemSize,
@@ -478,7 +478,7 @@ Builder addKeyline(
478478
*/
479479
@NonNull
480480
@CanIgnoreReturnValue
481-
Builder addKeyline(
481+
public Builder addKeyline(
482482
float offsetLoc,
483483
@FloatRange(from = 0.0F, to = 1.0F) float mask,
484484
float maskedItemSize,
@@ -520,7 +520,7 @@ Builder addKeyline(
520520
*/
521521
@NonNull
522522
@CanIgnoreReturnValue
523-
Builder addAnchorKeyline(
523+
public Builder addAnchorKeyline(
524524
float offsetLoc, @FloatRange(from = 0.0F, to = 1.0F) float mask, float maskedItemSize) {
525525
return addKeyline(
526526
offsetLoc, mask, maskedItemSize, /* isFocal= */ false, /* isAnchor= */ true);
@@ -535,7 +535,7 @@ Builder addAnchorKeyline(
535535
*/
536536
@NonNull
537537
@CanIgnoreReturnValue
538-
Builder addKeylineRange(
538+
public Builder addKeylineRange(
539539
float offsetLoc,
540540
@FloatRange(from = 0.0F, to = 1.0F) float mask,
541541
float maskedItemSize,
@@ -564,7 +564,7 @@ Builder addKeylineRange(
564564
*/
565565
@NonNull
566566
@CanIgnoreReturnValue
567-
Builder addKeylineRange(
567+
public Builder addKeylineRange(
568568
float offsetLoc,
569569
@FloatRange(from = 0.0F, to = 1.0F) float mask,
570570
float maskedItemSize,
@@ -584,7 +584,7 @@ Builder addKeylineRange(
584584

585585
/** Builds and returns a {@link KeylineState}. */
586586
@NonNull
587-
KeylineState build() {
587+
public KeylineState build() {
588588
if (tmpFirstFocalKeyline == null) {
589589
throw new IllegalStateException("There must be a keyline marked as focal.");
590590
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
* handle reversing a KeylineState when being laid out right-to-left before constructing a
4444
* KeylineStateList.
4545
*/
46-
class KeylineStateList {
46+
public class KeylineStateList {
4747

4848
private static final int NO_INDEX = -1;
4949

@@ -133,6 +133,7 @@ KeylineState getEndState() {
133133
* container as possible.
134134
* @return a {@link KeylineState} that has been shifted according on the scroll offset.
135135
*/
136+
@NonNull
136137
public KeylineState getShiftedState(
137138
float scrollOffset, float minScrollOffset, float maxScrollOffset) {
138139
return getShiftedState(scrollOffset, minScrollOffset, maxScrollOffset, false);

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ public final class MultiBrowseCarouselStrategy extends CarouselStrategy {
5757

5858
@Override
5959
@NonNull
60-
KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNull View child) {
60+
public KeylineState onFirstChildMeasuredWithMargins(
61+
@NonNull Carousel carousel, @NonNull View child) {
6162
float availableSpace = carousel.getContainerHeight();
6263
if (carousel.isHorizontal()) {
6364
availableSpace = carousel.getContainerWidth();
@@ -178,7 +179,7 @@ boolean ensureArrangementFitsItemCount(Arrangement arrangement, int carouselItem
178179
}
179180

180181
@Override
181-
boolean shouldRefreshKeylineState(Carousel carousel, int oldItemCount) {
182+
public boolean shouldRefreshKeylineState(@NonNull Carousel carousel, int oldItemCount) {
182183
return (oldItemCount < keylineCount && carousel.getItemCount() >= keylineCount)
183184
|| (oldItemCount >= keylineCount && carousel.getItemCount() < keylineCount);
184185
}

0 commit comments

Comments
 (0)