-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Carousel] Add Hero carousel strategy
PiperOrigin-RevId: 531247503
- Loading branch information
Showing
5 changed files
with
269 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
lib/java/com/google/android/material/carousel/HeroCarouselStrategy.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/* | ||
* Copyright 2023 The Android Open Source Project | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.google.android.material.carousel; | ||
|
||
import static com.google.android.material.carousel.CarouselStrategyHelper.createLeftAlignedKeylineState; | ||
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; | ||
import static java.lang.Math.max; | ||
import static java.lang.Math.min; | ||
|
||
import androidx.recyclerview.widget.RecyclerView.LayoutParams; | ||
import android.view.View; | ||
import androidx.annotation.NonNull; | ||
import androidx.core.math.MathUtils; | ||
|
||
/** | ||
* A {@link CarouselStrategy} that knows how to size and fit one large item and one small item into | ||
* a container to create a layout to browse one 'hero' item at a time with a preview item. | ||
* | ||
* <p>Note that this strategy resizes Carousel items to take up the full width of the Carousel, save | ||
* room for the small item. | ||
* | ||
* <p>This class will automatically be reversed by {@link CarouselLayoutManager} if being laid out | ||
* right-to-left and does not need to make any account for layout direction itself. | ||
*/ | ||
public class HeroCarouselStrategy extends CarouselStrategy { | ||
|
||
private static final int[] SMALL_COUNTS = new int[] {1}; | ||
private static final int[] MEDIUM_COUNTS = new int[] {0}; | ||
|
||
@Override | ||
@NonNull | ||
KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNull View child) { | ||
float availableSpace = carousel.getContainerWidth(); | ||
|
||
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams(); | ||
float childHorizontalMargins = childLayoutParams.leftMargin + childLayoutParams.rightMargin; | ||
|
||
float smallChildWidthMin = getSmallSizeMin(child.getContext()) + childHorizontalMargins; | ||
float smallChildWidthMax = getSmallSizeMax(child.getContext()) + childHorizontalMargins; | ||
|
||
float measuredChildWidth = availableSpace; | ||
float targetLargeChildWidth = min(measuredChildWidth + childHorizontalMargins, availableSpace); | ||
// Ideally we would like to create a balanced arrangement where a small item is 1/3 the size of | ||
// the large item. Clamp the small target size within our min-max range and as close to 1/3 of | ||
// the target large item size as possible. | ||
float targetSmallChildWidth = | ||
MathUtils.clamp( | ||
measuredChildWidth / 3F + childHorizontalMargins, | ||
getSmallSizeMin(child.getContext()) + childHorizontalMargins, | ||
getSmallSizeMax(child.getContext()) + childHorizontalMargins); | ||
float targetMediumChildWidth = (targetLargeChildWidth + targetSmallChildWidth) / 2F; | ||
|
||
// Find the minimum space left for large items after filling the carousel with the most | ||
// permissible small items to determine a plausible minimum large count. | ||
float minAvailableLargeSpace = | ||
availableSpace | ||
- (smallChildWidthMax * maxValue(SMALL_COUNTS)); | ||
int largeCountMin = (int) max(1, floor(minAvailableLargeSpace / targetLargeChildWidth)); | ||
int largeCountMax = (int) ceil(availableSpace / targetLargeChildWidth); | ||
int[] largeCounts = new int[largeCountMax - largeCountMin + 1]; | ||
for (int i = 0; i < largeCounts.length; i++) { | ||
largeCounts[i] = largeCountMin + i; | ||
} | ||
Arrangement arrangement = Arrangement.findLowestCostArrangement( | ||
availableSpace, | ||
targetSmallChildWidth, | ||
smallChildWidthMin, | ||
smallChildWidthMax, | ||
SMALL_COUNTS, | ||
targetMediumChildWidth, | ||
MEDIUM_COUNTS, | ||
targetLargeChildWidth, | ||
largeCounts); | ||
return createLeftAlignedKeylineState( | ||
child.getContext(), childHorizontalMargins, availableSpace, arrangement); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
lib/javatests/com/google/android/material/carousel/HeroCarouselStrategyTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/* | ||
* Copyright 2023 The Android Open Source Project | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package com.google.android.material.carousel; | ||
|
||
import com.google.android.material.test.R; | ||
|
||
import static com.google.android.material.carousel.CarouselHelper.createCarouselWithWidth; | ||
import static com.google.android.material.carousel.CarouselHelper.createViewWithSize; | ||
import static com.google.common.truth.Truth.assertThat; | ||
|
||
import androidx.recyclerview.widget.RecyclerView.LayoutParams; | ||
import android.view.View; | ||
import androidx.test.core.app.ApplicationProvider; | ||
import com.google.android.material.carousel.KeylineState.Keyline; | ||
import com.google.common.collect.Iterables; | ||
import java.util.List; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
import org.robolectric.RobolectricTestRunner; | ||
|
||
/** Tests for {@link HeroCarouselStrategy}. */ | ||
@RunWith(RobolectricTestRunner.class) | ||
public class HeroCarouselStrategyTest { | ||
|
||
@Test | ||
public void testItemSameAsContainerSize_showsOneLargeOneSmall() { | ||
Carousel carousel = createCarouselWithWidth(400); | ||
HeroCarouselStrategy config = new HeroCarouselStrategy(); | ||
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 400, 400); | ||
|
||
KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view); | ||
float minSmallItemSize = | ||
view.getResources().getDimension(R.dimen.m3_carousel_small_item_size_min); | ||
|
||
// 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 testItemSmallerThanContainer_showsOneLargeOneSmall() { | ||
Carousel carousel = createCarouselWithWidth(400); | ||
HeroCarouselStrategy config = new HeroCarouselStrategy(); | ||
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 100, 400); | ||
|
||
KeylineState keylineState = config.onFirstChildMeasuredWithMargins(carousel, view); | ||
float minSmallItemSize = | ||
view.getResources().getDimension(R.dimen.m3_carousel_small_item_size_min); | ||
|
||
// 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 testKnownArrangement_correctlyCalculatesKeylineLocations() { | ||
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 400, 200); | ||
|
||
HeroCarouselStrategy config = new HeroCarouselStrategy(); | ||
float extraSmallSize = | ||
view.getResources().getDimension(R.dimen.m3_carousel_gone_size); | ||
float minSmallItemSize = | ||
view.getResources().getDimension(R.dimen.m3_carousel_small_item_size_min); | ||
// Keyline sizes for the hero variant carousel are: | ||
// {extraSmallSize, largeSize, minSmallSize, extraSmallSize} | ||
// The keyline loc offsets are placed so that an item centered on a keyline has the | ||
// keyline size described above. | ||
// The large size is based on whatever width is left over from the minimum small size. | ||
float[] locOffsets = | ||
new float[] { | ||
-extraSmallSize / 2f, | ||
(400 - minSmallItemSize) / 2f, | ||
400 - minSmallItemSize / 2f, | ||
400 + extraSmallSize / 2f | ||
}; | ||
|
||
List<Keyline> keylines = | ||
config.onFirstChildMeasuredWithMargins(createCarouselWithWidth(400), view).getKeylines(); | ||
for (int i = 0; i < keylines.size(); i++) { | ||
assertThat(keylines.get(i).locOffset).isEqualTo(locOffsets[i]); | ||
} | ||
} | ||
|
||
@Test | ||
public void testKnownArrangementWithMargins_correctlyCalculatesKeylineLocations() { | ||
View view = createViewWithSize(ApplicationProvider.getApplicationContext(), 400, 200); | ||
LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); | ||
layoutParams.leftMargin += 50; | ||
layoutParams.rightMargin += 30; | ||
|
||
HeroCarouselStrategy config = new HeroCarouselStrategy(); | ||
float extraSmallSize = | ||
view.getResources().getDimension(R.dimen.m3_carousel_gone_size); | ||
float minSmallItemSize = | ||
view.getResources().getDimension(R.dimen.m3_carousel_small_item_size_min); | ||
// Keyline sizes for the hero variant carousel are: | ||
// {extraSmallSize, largeSize, minSmallSize, extraSmallSize} | ||
// The keyline loc offsets are placed so that an item centered on a keyline has the | ||
// keyline size described above. | ||
// The large size is based on whatever width is left over from the minimum small size. | ||
float[] locOffsets = | ||
new float[] { | ||
-(extraSmallSize + 80) / 2f, | ||
(400 - (minSmallItemSize + 80)) / 2f, | ||
400 - (minSmallItemSize + 80) / 2f, | ||
400 + (extraSmallSize + 80) / 2f | ||
}; | ||
|
||
List<Keyline> keylines = | ||
config.onFirstChildMeasuredWithMargins(createCarouselWithWidth(400), view).getKeylines(); | ||
for (int i = 0; i < keylines.size(); i++) { | ||
assertThat(keylines.get(i).locOffset).isEqualTo(locOffsets[i]); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters