Skip to content

Commit

Permalink
[Carousel] Add attributes to change small item size
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 580249803
  • Loading branch information
imhappi authored and dsn5ft committed Nov 8, 2023
1 parent 5055507 commit 92a5444
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 52 deletions.
10 changes: 6 additions & 4 deletions docs/components/Carousel.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,19 @@ the carousel size.

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

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

## Customizing carousel

### Item size

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.

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.

### Item shape

`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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ public void setCarouselStrategy(@NonNull CarouselStrategy carouselStrategy) {
@Override
public void onAttachedToWindow(RecyclerView view) {
super.onAttachedToWindow(view);
carouselStrategy.initialize(view.getContext());
refreshKeylineState();
view.addOnLayoutChangeListener(recyclerViewSizeChangeListener);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.android.material.carousel;

import android.content.Context;
import android.view.View;
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
Expand All @@ -26,6 +27,17 @@
*/
public abstract class CarouselStrategy {

private float smallSizeMin;

private float smallSizeMax;

void initialize(Context context) {
smallSizeMin =
smallSizeMin > 0 ? smallSizeMin : CarouselStrategyHelper.getSmallSizeMin(context);
smallSizeMax =
smallSizeMax > 0 ? smallSizeMax : CarouselStrategyHelper.getSmallSizeMax(context);
}

/**
* Calculates a keyline arrangement and returns a constructed {@link KeylineState}.
*
Expand Down Expand Up @@ -139,4 +151,45 @@ boolean shouldRefreshKeylineState(Carousel carousel, int oldItemCount) {
// state based on item count.
return false;
}

/**
* Sets the minimum size for the small items.
*
* <p> This method is a no-op for strategies that do not have small items.
*
* <p> Note that setting this size may impact other sizes in the carousel
* in order to fit the carousel strategy configuration.
* @param minSmallItemSize size to set the small item to.
*/
public void setSmallItemSizeMin(float minSmallItemSize) {
smallSizeMin = minSmallItemSize;
}

/**
* Sets the maximum size for the small items.
*
* <p> This method is a no-op for strategies that do not have small items.
*
* <p> Note that setting this size may impact other sizes in the carousel
* in order to fit the carousel strategy configuration.
* @param maxSmallItemSize size to set the small item to.
*/
public void setSmallItemSizeMax(float maxSmallItemSize) {
smallSizeMax = maxSmallItemSize;
}

/**
* Returns the minimum small item size value.
*/
public float getSmallItemSizeMin() {
return smallSizeMin;
}


/**
* Returns the maximum small item size value.
*/
public float getSmallItemSizeMax() {
return smallSizeMax;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
package com.google.android.material.carousel;

import static com.google.android.material.carousel.CarouselStrategyHelper.createKeylineState;
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMax;
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMin;
import static com.google.android.material.carousel.CarouselStrategyHelper.maxValue;
import static java.lang.Math.ceil;
import static java.lang.Math.floor;
Expand Down Expand Up @@ -72,8 +70,10 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
measuredChildSize = child.getMeasuredHeight() * 2;
}

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

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

int[] smallCounts = SMALL_COUNTS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
package com.google.android.material.carousel;

import static com.google.android.material.carousel.CarouselStrategyHelper.createKeylineState;
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMax;
import static com.google.android.material.carousel.CarouselStrategyHelper.getSmallSizeMin;
import static com.google.android.material.carousel.CarouselStrategyHelper.maxValue;
import static java.lang.Math.ceil;
import static java.lang.Math.floor;
Expand Down Expand Up @@ -74,8 +72,9 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul
measuredChildSize = child.getMeasuredWidth();
}

float smallChildSizeMin = getSmallSizeMin(child.getContext()) + childMargins;
float smallChildSizeMax = getSmallSizeMax(child.getContext()) + childMargins;
float smallChildSizeMin = getSmallItemSizeMin() + childMargins;
float smallChildSizeMax = getSmallItemSizeMax() + childMargins;
smallChildSizeMax = max(smallChildSizeMax, smallChildSizeMin);

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

// Create arrays representing the possible count of small, medium, and large items. These are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
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;

Expand Down Expand Up @@ -78,7 +77,7 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul

if (isCenter) {
remainingSpace /= 2F;
float smallChildSizeMin = getSmallSizeMin(child.getContext()) + childMargins;
float smallChildSizeMin = getSmallItemSizeMin() + 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,17 +236,17 @@ public int getItemCount() {
* Creates a {@link Carousel} with a specified {@code size} for both width and height and the
* specified alignment and orientation.
*/
static Carousel createCarousel(int size, int orientation, int alignment) {
static Carousel createCarousel(int width, int height, int orientation, int alignment) {
return new Carousel() {

@Override
public int getContainerWidth() {
return size;
return width;
}

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

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class HeroCarouselStrategyTest {
@Test
public void testItemSameAsContainerSize_showsOneLargeOneSmall() {
Carousel carousel = createCarouselWithWidth(400);
HeroCarouselStrategy config = new HeroCarouselStrategy();
HeroCarouselStrategy config = setupStrategy();
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 400, 400);

KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view);
Expand All @@ -61,7 +61,7 @@ public void testItemSameAsContainerSize_showsOneLargeOneSmall() {
@Test
public void testItemSmallerThanContainer_showsOneLargeOneSmall() {
Carousel carousel = createCarouselWithWidth(400);
HeroCarouselStrategy config = new HeroCarouselStrategy();
HeroCarouselStrategy config = setupStrategy();
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 100, 400);

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

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

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

HeroCarouselStrategy config = new HeroCarouselStrategy();
HeroCarouselStrategy config = setupStrategy();
float extraSmallSize =
view.getResources().getDimension(R.dimen.m3_carousel_gone_size);
float minSmallItemSize =
Expand Down Expand Up @@ -132,7 +132,7 @@ public void testKnownArrangementWithMargins_correctlyCalculatesKeylineLocations(
layoutParams.leftMargin += 50;
layoutParams.rightMargin += 30;

HeroCarouselStrategy config = new HeroCarouselStrategy();
HeroCarouselStrategy config = setupStrategy();
float extraSmallSize =
view.getResources().getDimension(R.dimen.m3_carousel_gone_size);
float minSmallItemSize =
Expand Down Expand Up @@ -167,13 +167,17 @@ public void testKnownCenterAlignmentArrangement_correctlyCalculatesKeylineLocati
ApplicationProvider.getApplicationContext(), (int) largeSize, (int) largeSize);
int carouselSize = (int) (largeSize + smallSize * 2);

HeroCarouselStrategy strategy = new HeroCarouselStrategy();
HeroCarouselStrategy strategy = setupStrategy();
List<Keyline> keylines =
strategy.onFirstChildMeasuredWithMargins(
createCarousel(
carouselSize,
CarouselLayoutManager.HORIZONTAL,
CarouselLayoutManager.ALIGNMENT_CENTER), view).getKeylines();
strategy
.onFirstChildMeasuredWithMargins(
createCarousel(
carouselSize,
carouselSize,
CarouselLayoutManager.HORIZONTAL,
CarouselLayoutManager.ALIGNMENT_CENTER),
view)
.getKeylines();

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

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

HeroCarouselStrategy strategy = new HeroCarouselStrategy();
HeroCarouselStrategy strategy = setupStrategy();
List<Keyline> keylines =
strategy
.onFirstChildMeasuredWithMargins(
Expand All @@ -212,4 +216,53 @@ public void testCenterAlignment_isLeftAlignedWithMinItems() {
assertThat(keylines.get(i).locOffset).isEqualTo(locOffsets[i]);
}
}

@Test
public void testSettingSmallRange_setsToMinSize() {
Carousel carousel = createCarouselWithWidth(400);
HeroCarouselStrategy config = setupStrategy();
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 400, 400);

float minSmallItemSize = 20;
config.setSmallItemSizeMin(minSmallItemSize);
config.setSmallItemSizeMax(1234);
KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view);

// A fullscreen layout should be [xSmall-large-small-xSmall] where the xSmall items are
// outside the bounds of the carousel container and the large center item takes up the
// containers full width.
assertThat(keylineState.getKeylines()).hasSize(4);
assertThat(keylineState.getKeylines().get(0).locOffset).isLessThan(0F);
assertThat(Iterables.getLast(keylineState.getKeylines()).locOffset)
.isGreaterThan((float) carousel.getContainerWidth());
assertThat(keylineState.getKeylines().get(1).mask).isEqualTo(0F);
assertThat(keylineState.getKeylines().get(2).maskedItemSize).isEqualTo(minSmallItemSize);
}

@Test
public void testLargeCarouselWidth_correctlyCalculatesKeylineLocations() {
int width = 400;
int height = 100;
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), width, height);

HeroCarouselStrategy config = setupStrategy();

List<Keyline> keylines =
config
.onFirstChildMeasuredWithMargins(
createCarousel(
width,
height,
CarouselLayoutManager.HORIZONTAL,
CarouselLayoutManager.ALIGNMENT_START),
view)
.getKeylines();
assertThat(keylines.get(1).maskedItemSize).isEqualTo(height * 2f);
}

private HeroCarouselStrategy setupStrategy() {
HeroCarouselStrategy strategy = new HeroCarouselStrategy();
strategy.initialize(ApplicationProvider.getApplicationContext());
return strategy;
}
}

0 comments on commit 92a5444

Please sign in to comment.