From 9393b9779e244cbd7c78522fd1282aed73cb8d33 Mon Sep 17 00:00:00 2001 From: rightnao Date: Mon, 15 Apr 2024 22:24:28 +0000 Subject: [PATCH] [Carousel] Support unclipped padding for uncontained variant of carousel PiperOrigin-RevId: 625101250 --- .../cat_carousel_uncontained_fragment.xml | 3 +- .../carousel/CarouselLayoutManager.java | 26 ++-- .../material/carousel/CarouselStrategy.java | 23 +++- .../material/carousel/KeylineStateList.java | 109 ++++++++++++--- .../carousel/UncontainedCarouselStrategy.java | 4 +- .../CarouselLayoutManagerRtlTest.java | 3 +- .../carousel/CarouselLayoutManagerTest.java | 49 ++++--- .../carousel/CarouselSnapHelperTest.java | 4 +- .../carousel/KeylineStateListTest.java | 124 ++++++++++++++---- 9 files changed, 252 insertions(+), 93 deletions(-) diff --git a/catalog/java/io/material/catalog/carousel/res/layout/cat_carousel_uncontained_fragment.xml b/catalog/java/io/material/catalog/carousel/res/layout/cat_carousel_uncontained_fragment.xml index 3719150bdad..31fdc4e9040 100644 --- a/catalog/java/io/material/catalog/carousel/res/layout/cat_carousel_uncontained_fragment.xml +++ b/catalog/java/io/material/catalog/carousel/res/layout/cat_carousel_uncontained_fragment.xml @@ -112,7 +112,8 @@ android:id="@+id/uncontained_carousel_recycler_view" android:layout_width="match_parent" android:layout_height="196dp" - android:layout_marginHorizontal="16dp" + android:paddingStart="16dp" + android:paddingEnd="16dp" android:layout_marginVertical="16dp" android:clipChildren="false" android:clipToPadding="false" /> diff --git a/lib/java/com/google/android/material/carousel/CarouselLayoutManager.java b/lib/java/com/google/android/material/carousel/CarouselLayoutManager.java index 7bd21ee2a7a..97ccdbfcbdf 100644 --- a/lib/java/com/google/android/material/carousel/CarouselLayoutManager.java +++ b/lib/java/com/google/android/material/carousel/CarouselLayoutManager.java @@ -55,6 +55,7 @@ import androidx.core.math.MathUtils; import androidx.core.util.Preconditions; import androidx.core.view.ViewCompat; +import com.google.android.material.carousel.CarouselStrategy.StrategyType; import com.google.android.material.carousel.KeylineState.Keyline; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -98,6 +99,7 @@ public class CarouselLayoutManager extends LayoutManager private boolean isDebuggingEnabled = false; private final DebugItemDecoration debugItemDecoration = new DebugItemDecoration(); @NonNull private CarouselStrategy carouselStrategy; + @Nullable private KeylineStateList keylineStateList; // A KeylineState shifted for any current scroll offset. @Nullable private KeylineState currentKeylineState; @@ -227,8 +229,7 @@ public int getCarouselAlignment() { private int getLeftOrTopPaddingForKeylineShift() { // TODO(b/316969331): Fix keyline shifting by decreasing carousel size when carousel is clipped // to padding. - // TODO(b/316968490): Fix keyline shifting by adjusting cutoffs if strategy is not contained. - if (getClipToPadding() || !carouselStrategy.isContained()) { + if (getClipToPadding()) { return 0; } if (getOrientation() == VERTICAL) { @@ -240,8 +241,7 @@ private int getLeftOrTopPaddingForKeylineShift() { private int getRightOrBottomPaddingForKeylineShift() { // TODO(b/316969331): Fix keyline shifting by decreasing carousel size when carousel is clipped // to padding. - // TODO(b/316968490): Fix keyline shifting by adjusting cutoffs if strategy is not contained. - if (getClipToPadding() || !carouselStrategy.isContained()) { + if (getClipToPadding()) { return 0; } if (getOrientation() == VERTICAL) { @@ -348,7 +348,8 @@ private void recalculateKeylineStateList(Recycler recycler) { isLayoutRtl() ? KeylineState.reverse(keylineState, getContainerSize()) : keylineState, getItemMargins(), getLeftOrTopPaddingForKeylineShift(), - getRightOrBottomPaddingForKeylineShift()); + getRightOrBottomPaddingForKeylineShift(), + carouselStrategy.getStrategyType()); } private int getItemMargins() { @@ -853,10 +854,7 @@ private int calculateEndScroll(State state, KeylineStateList stateList) { float lastItemDistanceFromFirstItem = ((state.getItemCount() - 1) * endState.getItemSize()) * (isRtl ? -1F : 1F); - float endPadding = - isRtl ? -endFocalKeyline.leftOrTopPaddingShift : endFocalKeyline.rightOrBottomPaddingShift; float endFocalLocDistanceFromStart = endFocalKeyline.loc - getParentStart(); - float endFocalLocDistanceFromEnd = getParentEnd() - endFocalKeyline.loc; // We want the last item in the list to only be able to scroll to the end of the list. Subtract // the distance to the end focal keyline and then add the distance needed to let the last @@ -865,11 +863,7 @@ private int calculateEndScroll(State state, KeylineStateList stateList) { (int) (lastItemDistanceFromFirstItem - endFocalLocDistanceFromStart - + endFocalLocDistanceFromEnd - // If there is padding, adjust for the extra padding offset since offset is - // implicitly added from both endFocalLocDistance calculations. - + endPadding); - + + (isRtl ? -1 : 1) * endFocalKeyline.maskedItemSize / 2F); return isRtl ? min(0, endScroll) : max(0, endScroll); } @@ -994,7 +988,7 @@ private void updateChildMaskForLocation( // container instead of bleeding and being clipped by the RecyclerView's bounds. // Only do this if there is only one side of the mask that is out of bounds; if // both sides are out of bounds on the same side, then the whole mask is out of view. - if (carouselStrategy.isContained()) { + if (carouselStrategy.getStrategyType() == StrategyType.CONTAINED) { orientationHelper.containMaskWithinBounds(maskRect, offsetMaskRect, parentBoundsRect); } @@ -1060,10 +1054,6 @@ private int getParentRight() { return orientationHelper.getParentRight(); } - private int getParentEnd() { - return orientationHelper.getParentEnd(); - } - private int getParentTop() { return orientationHelper.getParentTop(); } diff --git a/lib/java/com/google/android/material/carousel/CarouselStrategy.java b/lib/java/com/google/android/material/carousel/CarouselStrategy.java index 38f51b5cb1e..c743beea116 100644 --- a/lib/java/com/google/android/material/carousel/CarouselStrategy.java +++ b/lib/java/com/google/android/material/carousel/CarouselStrategy.java @@ -31,6 +31,15 @@ public abstract class CarouselStrategy { private float smallSizeMax; + /** + * Enum that defines whether or not the strategy is contained or uncontained. Contained strategies + * will always have all of its items within bounds of the carousel width. + */ + enum StrategyType { + CONTAINED, + UNCONTAINED + } + void initialize(Context context) { smallSizeMin = smallSizeMin > 0 ? smallSizeMin : CarouselStrategyHelper.getSmallSizeMin(context); @@ -130,14 +139,16 @@ static int[] doubleCounts(int[] count) { } /** - * Gets whether this carousel should mask items against the edges of the carousel container. + * Gets the strategy type of this strategy. Contained strategies should mask items against the + * edges of the carousel container. * - * @return true if items in the carousel should mask/squash against the edges of the carousel - * container. false if the carousel should allow items to bleed past the edges of the - * container and be clipped. + * @return the {@link StrategyType} of this strategy. A value of {@link StrategyType#CONTAINED} + * means items in the carousel should mask/squash against the edges of the carousel container. + * {@link StrategyType#UNCONTAINED} means the carousel should allow items to bleed past the edges + * of the container and be clipped. */ - boolean isContained() { - return true; + StrategyType getStrategyType() { + return StrategyType.CONTAINED; } /** diff --git a/lib/java/com/google/android/material/carousel/KeylineStateList.java b/lib/java/com/google/android/material/carousel/KeylineStateList.java index 46fa794f89c..168ac85694c 100644 --- a/lib/java/com/google/android/material/carousel/KeylineStateList.java +++ b/lib/java/com/google/android/material/carousel/KeylineStateList.java @@ -16,6 +16,9 @@ package com.google.android.material.carousel; +import static java.lang.Math.max; +import static java.lang.Math.min; + import androidx.annotation.NonNull; import androidx.core.math.MathUtils; import com.google.android.material.animation.AnimationUtils; @@ -82,11 +85,17 @@ private KeylineStateList( } /** Creates a new {@link KeylineStateList} from a {@link KeylineState}. */ - static KeylineStateList from(Carousel carousel, KeylineState state, float itemMargins, - float leftOrTopPadding, float rightOrBottomPadding) { + static KeylineStateList from( + Carousel carousel, + KeylineState state, + float itemMargins, + float leftOrTopPadding, + float rightOrBottomPadding, + CarouselStrategy.StrategyType strategyType) { return new KeylineStateList( - state, getStateStepsStart(carousel, state, itemMargins, leftOrTopPadding), - getStateStepsEnd(carousel, state, itemMargins, rightOrBottomPadding)); + state, + getStateStepsStart(carousel, state, itemMargins, leftOrTopPadding, strategyType), + getStateStepsEnd(carousel, state, itemMargins, rightOrBottomPadding, strategyType)); } /** Returns the default state for this state list. */ @@ -155,14 +164,17 @@ KeylineState getShiftedState( float startShiftOffset = minScrollOffset + startShiftRange; float endShiftOffset = maxScrollOffset - endShiftRange; float startPaddingShift = getStartState().getFirstFocalKeyline().leftOrTopPaddingShift; - float endPaddingShift = getEndState().getLastFocalKeyline().rightOrBottomPaddingShift; + float endPaddingShift = getEndState().getFirstFocalKeyline().rightOrBottomPaddingShift; // Normally we calculate the interpolation such that by scrollShiftOffset, we are always at the - // default state but in this case, we want to start shifting earlier/increase startShiftOffset - // so that the interpolation will choose the start state instead of the default state when the - // scroll offset is equal to startPaddingShift. This is so we are always at the start state - // at the beginning of the carousel, instead of getting to a state where the start state is only + // default state. In the case where the start state is equal to the default state but with + // padding, we want to start shifting earlier/increase startShiftOffset so that the + // interpolation will choose the start state instead of the default state when the scroll offset + // is equal to startPaddingShift. This is so we are always at the start state with padding at + // the beginning of the carousel, instead of getting to a state where the start state is only // when scrollOffset <= startPaddingShift. + // We know that the start state is equal to the default state with padding if the start shift + // range is equal to the padding. if (startShiftRange == startPaddingShift) { startShiftOffset += startPaddingShift; } @@ -362,7 +374,56 @@ private static boolean isLastFocalItemVisibleAtRightOfContainer( && state.getLastFocalKeyline() == state.getLastNonAnchorKeyline(); } + @NonNull private static KeylineState shiftKeylineStateForPadding( + @NonNull KeylineState keylineState, float padding, float carouselSize, boolean leftShift, + float childMargins, CarouselStrategy.StrategyType strategyType) { + switch (strategyType) { + case CONTAINED: + return shiftKeylineStateForPaddingContained( + keylineState, padding, carouselSize, leftShift, childMargins); + default: + return shiftKeylineStateForPaddingUncontained( + keylineState, padding, carouselSize, leftShift); + } + } + + @NonNull + private static KeylineState shiftKeylineStateForPaddingUncontained( + @NonNull KeylineState keylineState, float padding, float carouselSize, boolean leftShift) { + List tmpKeylines = new ArrayList<>(keylineState.getKeylines()); + KeylineState.Builder builder = + new KeylineState.Builder(keylineState.getItemSize(), carouselSize); + int unchangingAnchorPosition = leftShift ? 0 : tmpKeylines.size() - 1; + for (int j = 0; j < tmpKeylines.size(); j++) { + Keyline k = tmpKeylines.get(j); + if (k.isAnchor && j == unchangingAnchorPosition) { + builder.addKeyline(k.locOffset, k.mask, k.maskedItemSize, false, true, k.cutoff); + continue; + } + float newOffset = leftShift ? k.locOffset + padding : k.locOffset - padding; + float leftOrTopPadding = leftShift ? padding : 0; + float rightOrBottomPadding = leftShift ? 0 : padding; + boolean isFocal = + j >= keylineState.getFirstFocalKeylineIndex() + && j <= keylineState.getLastFocalKeylineIndex(); + builder.addKeyline( + newOffset, + k.mask, + k.maskedItemSize, + isFocal, + k.isAnchor, + Math.abs( + leftShift + ? max(0, newOffset + k.maskedItemSize / 2 - carouselSize) + : min(0, newOffset - k.maskedItemSize / 2)), + leftOrTopPadding, + rightOrBottomPadding); + } + return builder.build(); + } + + private static KeylineState shiftKeylineStateForPaddingContained( KeylineState keylineState, float padding, float carouselSize, boolean leftShift, float childMargins) { @@ -400,7 +461,7 @@ private static KeylineState shiftKeylineStateForPadding( maskedItemSize, keylineState.getItemSize(), childMargins); float locOffset = nextOffset + maskedItemSize / 2F; - float actualPaddingShift = locOffset - k.locOffset; + float actualPaddingShift = Math.abs(locOffset - k.locOffset); builder.addKeyline( locOffset, @@ -434,7 +495,7 @@ private static KeylineState shiftKeylineStateForPadding( */ private static List getStateStepsStart( Carousel carousel, KeylineState defaultState, float itemMargins, - float leftOrTopPaddingForKeylineShift) { + float leftOrTopPaddingForKeylineShift, CarouselStrategy.StrategyType strategyType) { List steps = new ArrayList<>(); steps.add(defaultState); int firstNonAnchorKeylineIndex = findFirstNonAnchorKeylineIndex(defaultState); @@ -453,7 +514,8 @@ private static List getStateStepsStart( leftOrTopPaddingForKeylineShift, carouselSize, true, - itemMargins)); + itemMargins, + strategyType)); } return steps; } @@ -471,8 +533,10 @@ private static List getStateStepsStart( // view. float cutoffs = defaultState.getFirstFocalKeyline().cutoff; steps.add( - shiftKeylinesAndCreateKeylineState(defaultState, originalStart + cutoffs, carouselSize)); - // TODO(b/316968490): If there is padding, this should affect keylines and the cutoffs + shiftKeylinesAndCreateKeylineState( + defaultState, + originalStart + cutoffs + leftOrTopPaddingForKeylineShift, + carouselSize)); return steps; } @@ -512,7 +576,8 @@ private static List getStateStepsStart( leftOrTopPaddingForKeylineShift, carouselSize, true, - itemMargins); + itemMargins, + strategyType); } steps.add(shifted); } @@ -536,7 +601,8 @@ private static List getStateStepsStart( * carousel. */ private static List getStateStepsEnd(Carousel carousel, KeylineState defaultState, - float itemMargins, float rightOrBottomPaddingForKeylineShift) { + float itemMargins, float rightOrBottomPaddingForKeylineShift, + CarouselStrategy.StrategyType strategyType) { List steps = new ArrayList<>(); steps.add(defaultState); int lastNonAnchorKeylineIndex = findLastNonAnchorKeylineIndex(defaultState); @@ -556,7 +622,8 @@ private static List getStateStepsEnd(Carousel carousel, KeylineSta rightOrBottomPaddingForKeylineShift, carouselSize, false, - itemMargins)); + itemMargins, + strategyType)); } return steps; } @@ -573,9 +640,8 @@ private static List getStateStepsEnd(Carousel carousel, KeylineSta // view. Add a step that shifts all the keylines over to bring the last focal item into full // view. float cutoffs = defaultState.getLastFocalKeyline().cutoff; - steps.add( - shiftKeylinesAndCreateKeylineState(defaultState, originalStart - cutoffs, carouselSize)); - // TODO(b/316968490): If there is padding, this should affect keylines and the cutoffs + steps.add(shiftKeylinesAndCreateKeylineState(defaultState, + originalStart - cutoffs - rightOrBottomPaddingForKeylineShift, carouselSize)); return steps; } @@ -614,7 +680,8 @@ private static List getStateStepsEnd(Carousel carousel, KeylineSta rightOrBottomPaddingForKeylineShift, carouselSize, false, - itemMargins); + itemMargins, + strategyType); } steps.add(shifted); } diff --git a/lib/java/com/google/android/material/carousel/UncontainedCarouselStrategy.java b/lib/java/com/google/android/material/carousel/UncontainedCarouselStrategy.java index a546af9f921..a5161020007 100644 --- a/lib/java/com/google/android/material/carousel/UncontainedCarouselStrategy.java +++ b/lib/java/com/google/android/material/carousel/UncontainedCarouselStrategy.java @@ -237,7 +237,7 @@ private KeylineState createLeftAlignedKeylineState( } @Override - boolean isContained() { - return false; + StrategyType getStrategyType() { + return StrategyType.UNCONTAINED; } } diff --git a/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerRtlTest.java b/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerRtlTest.java index 2c184a73358..7f70e938486 100644 --- a/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerRtlTest.java +++ b/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerRtlTest.java @@ -40,6 +40,7 @@ import androidx.test.core.app.ApplicationProvider; import com.google.android.material.carousel.CarouselHelper.CarouselTestAdapter; import com.google.android.material.carousel.CarouselHelper.WrappedCarouselLayoutManager; +import com.google.android.material.carousel.CarouselStrategy.StrategyType; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -100,7 +101,7 @@ public void testScrollBeyondMaxHorizontalScroll_shouldLimitToMaxScrollOffset() t KeylineState leftState = KeylineStateList.from( layoutManager, KeylineState.reverse(keylineState, DEFAULT_RECYCLER_VIEW_WIDTH), - 0, 0, 0) + 0, 0, 0, StrategyType.CONTAINED) .getStartState(); MaskableFrameLayout child = diff --git a/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java b/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java index 0ac03fdd1e8..238472ecda7 100644 --- a/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java +++ b/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java @@ -40,6 +40,7 @@ import com.google.android.material.carousel.CarouselHelper.CarouselTestAdapter; import com.google.android.material.carousel.CarouselHelper.TestItem; import com.google.android.material.carousel.CarouselHelper.WrappedCarouselLayoutManager; +import com.google.android.material.carousel.CarouselStrategy.StrategyType; import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Test; @@ -174,7 +175,8 @@ KeylineState onFirstChildMeasuredWithMargins( scrollToPosition(recyclerView, layoutManager, 200); KeylineState endState = - KeylineStateList.from(layoutManager, keylineState, 0, 0, 0).getEndState(); + KeylineStateList.from(layoutManager, keylineState, 0, 0, 0, StrategyType.CONTAINED) + .getEndState(); MaskableFrameLayout child = (MaskableFrameLayout) recyclerView.getChildAt(recyclerView.getChildCount() - 1); @@ -219,7 +221,8 @@ KeylineState onFirstChildMeasuredWithMargins( scrollToPosition(recyclerView, layoutManager, 200); KeylineState endState = - KeylineStateList.from(layoutManager, keylineState, 0, 0, 0).getEndState(); + KeylineStateList.from(layoutManager, keylineState, 0, 0, 0, StrategyType.CONTAINED) + .getEndState(); MaskableFrameLayout child = (MaskableFrameLayout) recyclerView.getChildAt(recyclerView.getChildCount() - 1); @@ -370,7 +373,8 @@ public void testScrollToEndThenToStartVertical_childrenHaveValidOrder() throws T @Test public void testContainedLayout_doesNotAllowFirstItemToBleed() throws Throwable { - layoutManager.setCarouselStrategy(new TestContainmentCarouselStrategy(/* isContained= */ true)); + layoutManager.setCarouselStrategy( + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.CONTAINED)); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); scrollHorizontallyBy(recyclerView, layoutManager, 900); @@ -380,7 +384,8 @@ public void testContainedLayout_doesNotAllowFirstItemToBleed() throws Throwable @Test public void testContainedLayoutVertical_doesNotAllowFirstItemToBleed() throws Throwable { - layoutManager.setCarouselStrategy(new TestContainmentCarouselStrategy(/* isContained= */ true)); + layoutManager.setCarouselStrategy( + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.CONTAINED)); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); setVerticalOrientation(recyclerView, layoutManager); scrollVerticallyBy(recyclerView, layoutManager, 900); @@ -392,7 +397,8 @@ public void testContainedLayoutVertical_doesNotAllowFirstItemToBleed() throws Th @Test public void testContainedLayout_doesNotAllowLastItemToBleed() throws Throwable { - layoutManager.setCarouselStrategy(new TestContainmentCarouselStrategy(/* isContained= */ true)); + layoutManager.setCarouselStrategy( + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.CONTAINED)); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); scrollToPosition(recyclerView, layoutManager, 5); scrollHorizontallyBy(recyclerView, layoutManager, -165); @@ -403,7 +409,8 @@ public void testContainedLayout_doesNotAllowLastItemToBleed() throws Throwable { @Test public void testContainedLayoutVertical_doesNotAllowLastItemToBleed() throws Throwable { - layoutManager.setCarouselStrategy(new TestContainmentCarouselStrategy(/* isContained= */ true)); + layoutManager.setCarouselStrategy( + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.CONTAINED)); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); scrollToPosition(recyclerView, layoutManager, 5); setVerticalOrientation(recyclerView, layoutManager); @@ -416,7 +423,7 @@ public void testContainedLayoutVertical_doesNotAllowLastItemToBleed() throws Thr @Test public void testUncontainedLayout_allowsFistItemToBleed() throws Throwable { layoutManager.setCarouselStrategy( - new TestContainmentCarouselStrategy(/* isContained= */ false)); + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.UNCONTAINED)); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); scrollHorizontallyBy(recyclerView, layoutManager, 900); @@ -427,7 +434,7 @@ public void testUncontainedLayout_allowsFistItemToBleed() throws Throwable { @Test public void testUncontainedLayoutVertical_allowsFirstItemToBleed() throws Throwable { layoutManager.setCarouselStrategy( - new TestContainmentCarouselStrategy(/* isContained= */ false)); + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.UNCONTAINED)); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); setVerticalOrientation(recyclerView, layoutManager); scrollVerticallyBy(recyclerView, layoutManager, 30); @@ -440,7 +447,7 @@ public void testUncontainedLayoutVertical_allowsFirstItemToBleed() throws Throwa @Test public void testUncontainedLayout_allowsLastItemToBleed() throws Throwable { layoutManager.setCarouselStrategy( - new TestContainmentCarouselStrategy(/* isContained= */ false)); + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.UNCONTAINED)); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); scrollHorizontallyBy(recyclerView, layoutManager, 900); @@ -451,7 +458,7 @@ public void testUncontainedLayout_allowsLastItemToBleed() throws Throwable { @Test public void testUncontainedLayoutVertical_allowsLastItemToBleed() throws Throwable { layoutManager.setCarouselStrategy( - new TestContainmentCarouselStrategy(/* isContained= */ false)); + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.UNCONTAINED)); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); setVerticalOrientation(recyclerView, layoutManager); scrollVerticallyBy(recyclerView, layoutManager, 900); @@ -464,7 +471,7 @@ public void testUncontainedLayoutVertical_allowsLastItemToBleed() throws Throwab @Test public void testMasksLeftOfParent_areRoundedDown() throws Throwable { layoutManager.setCarouselStrategy( - new TestContainmentCarouselStrategy(/* isContained= */ false)); + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.UNCONTAINED)); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); scrollHorizontallyBy(recyclerView, layoutManager, 900); @@ -478,7 +485,7 @@ public void testMasksLeftOfParent_areRoundedDown() throws Throwable { @Test public void testMaskOnLeftParentEdge_areRoundedDown() throws Throwable { layoutManager.setCarouselStrategy( - new TestContainmentCarouselStrategy(/* isContained= */ false)); + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.UNCONTAINED)); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); // Scroll to end scrollToPosition(recyclerView, layoutManager, 9); @@ -495,7 +502,7 @@ public void testMaskOnLeftParentEdge_areRoundedDown() throws Throwable { @Test public void testMasksTopOfParent_areRoundedDown() throws Throwable { layoutManager.setCarouselStrategy( - new TestContainmentCarouselStrategy(/* isContained= */ false)); + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.UNCONTAINED)); setVerticalOrientation(recyclerView, layoutManager); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); scrollVerticallyBy(recyclerView, layoutManager, 900); @@ -510,7 +517,7 @@ public void testMasksTopOfParent_areRoundedDown() throws Throwable { @Test public void testMaskOnTopParentEdge_areRoundedDown() throws Throwable { layoutManager.setCarouselStrategy( - new TestContainmentCarouselStrategy(/* isContained= */ false)); + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.UNCONTAINED)); setVerticalOrientation(recyclerView, layoutManager); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); // Scroll to end @@ -541,7 +548,7 @@ public void testMaskOnTopParentEdge_areRoundedDown() throws Throwable { @Test public void testMaskOnRightParentEdge_areRoundedUp() throws Throwable { layoutManager.setCarouselStrategy( - new TestContainmentCarouselStrategy(/* isContained= */ false)); + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.UNCONTAINED)); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); // Carousel strategy is {large, small}. First child will be large item, second child will @@ -556,7 +563,7 @@ public void testMaskOnRightParentEdge_areRoundedUp() throws Throwable { @Test public void testMaskOnBottomParentEdge_areRoundedUp() throws Throwable { layoutManager.setCarouselStrategy( - new TestContainmentCarouselStrategy(/* isContained= */ false)); + new TestContainmentCarouselStrategy(/* strategyType= */ StrategyType.UNCONTAINED)); setVerticalOrientation(recyclerView, layoutManager); setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); @@ -683,10 +690,10 @@ private Rect getMaskRectOffsetToRecyclerViewCoords(MaskableFrameLayout child) { */ private static class TestContainmentCarouselStrategy extends CarouselStrategy { - private final boolean isContained; + private final StrategyType strategyType; - TestContainmentCarouselStrategy(boolean isContained) { - this.isContained = isContained; + TestContainmentCarouselStrategy(StrategyType strategyType) { + this.strategyType = strategyType; } @Override @@ -713,8 +720,8 @@ KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNul } @Override - boolean isContained() { - return isContained; + StrategyType getStrategyType() { + return strategyType; } } } diff --git a/lib/javatests/com/google/android/material/carousel/CarouselSnapHelperTest.java b/lib/javatests/com/google/android/material/carousel/CarouselSnapHelperTest.java index a4dcdc07d8b..db2b9138589 100644 --- a/lib/javatests/com/google/android/material/carousel/CarouselSnapHelperTest.java +++ b/lib/javatests/com/google/android/material/carousel/CarouselSnapHelperTest.java @@ -30,6 +30,7 @@ import androidx.test.core.app.ApplicationProvider; import com.google.android.material.carousel.CarouselHelper.CarouselTestAdapter; import com.google.android.material.carousel.CarouselHelper.WrappedCarouselLayoutManager; +import com.google.android.material.carousel.CarouselStrategy.StrategyType; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -134,7 +135,8 @@ public void testSnap_correctDistance() throws Throwable { // original snap distance if keylines stayed the same) minus the difference in focal keyline // location between keyline states. KeylineStateList stateList = - KeylineStateList.from(layoutManager, getTestCenteredKeylineState(), 0, 0, 0); + KeylineStateList.from( + layoutManager, getTestCenteredKeylineState(), 0, 0, 0, StrategyType.CONTAINED); KeylineState target1 = stateList.getShiftedState( horizontalScrollBefore, diff --git a/lib/javatests/com/google/android/material/carousel/KeylineStateListTest.java b/lib/javatests/com/google/android/material/carousel/KeylineStateListTest.java index 1c53c68bf4f..d712fe24e87 100644 --- a/lib/javatests/com/google/android/material/carousel/KeylineStateListTest.java +++ b/lib/javatests/com/google/android/material/carousel/KeylineStateListTest.java @@ -21,6 +21,7 @@ import static com.google.android.material.carousel.CarouselHelper.getKeylineMaskPercentage; import static com.google.common.truth.Truth.assertThat; +import com.google.android.material.carousel.CarouselStrategy.StrategyType; import com.google.android.material.carousel.KeylineState.Keyline; import java.util.List; import java.util.Map; @@ -51,7 +52,7 @@ public void testCenterArrangement_shouldShiftStart() { .addKeyline(135F, getKeylineMaskPercentage(10F, 40F), 10F) .build(); KeylineStateList stateList = - KeylineStateList.from(createCarouselWithWidth(140), state, 0, 0, 0); + KeylineStateList.from(createCarouselWithWidth(140), state, 0, 0, 0, StrategyType.CONTAINED); float[] scrollSteps = new float[] {50F, 20F, 0F}; float minScroll = 0F; float maxScroll = 5 * 40F; @@ -81,7 +82,7 @@ public void testCenterArrangement_shouldCreateIntermediaryStates() { .addKeyline(135F, getKeylineMaskPercentage(10F, 40F), 10F) .build(); KeylineStateList stateList = - KeylineStateList.from(createCarouselWithWidth(140), state, 0, 0, 0); + KeylineStateList.from(createCarouselWithWidth(140), state, 0, 0, 0, StrategyType.CONTAINED); float[] scrollOffsets = new float[] {35F, 10F}; float minScroll = 0F; float maxScroll = 5 * 40F; @@ -113,7 +114,7 @@ public void testCenterArrangement_shouldShiftEnd() { .addKeyline(135F, getKeylineMaskPercentage(10F, 40F), 10F) .build(); KeylineStateList stateList = - KeylineStateList.from(createCarouselWithWidth(140), state, 0, 0, 0); + KeylineStateList.from(createCarouselWithWidth(140), state, 0, 0, 0, StrategyType.CONTAINED); float[] scrollSteps = new float[] {50F, 20F, 0F}; float minScroll = 0F; float maxScroll = 5 * 40F; @@ -137,7 +138,7 @@ public void testCenterArrangement_shouldNotShift() { .addKeyline(135F, getKeylineMaskPercentage(10F, 40F), 10F) .build(); KeylineStateList stateList = - KeylineStateList.from(createCarouselWithWidth(140), state, 0, 0, 0); + KeylineStateList.from(createCarouselWithWidth(140), state, 0, 0, 0, StrategyType.CONTAINED); float minScroll = 0F; float maxScroll = 5 * 40F; @@ -153,7 +154,8 @@ public void testStartArrangement_shouldShiftStart() { .addKeyline(50F, getKeylineMaskPercentage(20F, 40F), 20F) .addKeyline(65F, getKeylineMaskPercentage(10F, 40F), 10F) .build(); - KeylineStateList stateList = KeylineStateList.from(createCarouselWithWidth(70), state, 0, 0, 0); + KeylineStateList stateList = + KeylineStateList.from(createCarouselWithWidth(70), state, 0, 0, 0, StrategyType.CONTAINED); float[] locOffsets = new float[] {20F, 50F, 65F}; @@ -178,7 +180,8 @@ public void testStartArrangement_shouldShiftEnd() { .addKeyline(50F, getKeylineMaskPercentage(20F, 40F), 20F) .addKeyline(65F, getKeylineMaskPercentage(10F, 40F), 10F) .build(); - KeylineStateList stateList = KeylineStateList.from(createCarouselWithWidth(70), state, 0, 0, 0); + KeylineStateList stateList = + KeylineStateList.from(createCarouselWithWidth(70), state, 0, 0, 0, StrategyType.CONTAINED); float[] scrollSteps = new float[] {50F, 20F, 0F}; float minScroll = 0F; float maxScroll = 2 * 40F; @@ -207,7 +210,8 @@ public void testStartArrangementWithOutOfBoundsKeylines_shouldShiftStart() { .addKeyline(70F, getKeylineMaskPercentage(20F, 40F), 20F) .addKeyline(90F, getKeylineMaskPercentage(20F, 40F), 20F) .build(); - KeylineStateList stateList = KeylineStateList.from(createCarouselWithWidth(90), state, 0, 0, 0); + KeylineStateList stateList = + KeylineStateList.from(createCarouselWithWidth(90), state, 0, 0, 0, StrategyType.CONTAINED); float[] scrollSteps = new float[] {20F, 0F}; float minScroll = 0F; @@ -242,7 +246,7 @@ public void testStartArrangementWithOutOfBoundsKeyline_shouldShiftEnd() { .addKeyline(130F, getKeylineMaskPercentage(20F, 40F), 20F) .build(); KeylineStateList stateList = - KeylineStateList.from(createCarouselWithWidth(100), state, 0, 0, 0); + KeylineStateList.from(createCarouselWithWidth(100), state, 0, 0, 0, StrategyType.CONTAINED); float[] scrollSteps = new float[] {40F, 20F, 0F}; float minScroll = 0F; @@ -271,7 +275,8 @@ public void testEndArrangement_shouldShiftStart() { .addKeyline(20F, getKeylineMaskPercentage(20F, 40F), 20F) .addKeyline(50F, 0F, 40F, true) .build(); - KeylineStateList stateList = KeylineStateList.from(createCarouselWithWidth(70), state, 0, 0, 0); + KeylineStateList stateList = + KeylineStateList.from(createCarouselWithWidth(70), state, 0, 0, 0, StrategyType.CONTAINED); float[] scrollSteps = new float[] {50F, 20F, 0F}; float minScroll = 0F; float maxScroll = 5 * 40F; @@ -292,7 +297,8 @@ public void testEndArrangement_shouldNotShiftEnd() { .addKeyline(20F, getKeylineMaskPercentage(20F, 40F), 20F) .addKeyline(50F, 0F, 40F, true) .build(); - KeylineStateList stateList = KeylineStateList.from(createCarouselWithWidth(70), state, 0, 0, 0); + KeylineStateList stateList = + KeylineStateList.from(createCarouselWithWidth(70), state, 0, 0, 0, StrategyType.CONTAINED); float minScroll = 0F; float maxScroll = 5 * 40F; @@ -310,7 +316,8 @@ public void testFullScreenArrangementWithAnchorKeylines_nothingShifts() { .addKeyline(20F, 0F, 40F, true) .addAnchorKeyline(45F, getKeylineMaskPercentage(10F, 40F), 10F) .build(); - KeylineStateList stateList = KeylineStateList.from(createCarouselWithWidth(40), state, 0, 0, 0); + KeylineStateList stateList = + KeylineStateList.from(createCarouselWithWidth(40), state, 0, 0, 0, StrategyType.CONTAINED); List startStep = stateList.getStartState().getKeylines(); List endStep = stateList.getEndState().getKeylines(); @@ -329,7 +336,7 @@ public void testMultipleFocalItems_shiftsFocalRange() { .addKeyline(475F, .5F, 50F) .build(); KeylineStateList stateList = - KeylineStateList.from(createCarouselWithWidth(500), state, 0, 0, 0); + KeylineStateList.from(createCarouselWithWidth(500), state, 0, 0, 0, StrategyType.CONTAINED); assertThat(stateList.getStartState().getFirstFocalKeylineIndex()).isEqualTo(0); assertThat(stateList.getStartState().getLastFocalKeylineIndex()).isEqualTo(3); @@ -344,7 +351,7 @@ public void testKeylineStateForPosition() { .addKeyline(475F, .5F, 50F) .build(); KeylineStateList stateList = - KeylineStateList.from(createCarouselWithWidth(500), state, 0, 0, 0); + KeylineStateList.from(createCarouselWithWidth(500), state, 0, 0, 0, StrategyType.CONTAINED); int itemCount = 10; Map positionMap = stateList.getKeylineStateForPositionMap(itemCount, 0, 1000, false); @@ -373,7 +380,7 @@ public void testKeylineStateForPositionRTL() { .addKeyline(475F, .5F, 50F) .build(); KeylineStateList stateList = - KeylineStateList.from(createCarouselWithWidth(500), state, 0, 0, 0); + KeylineStateList.from(createCarouselWithWidth(500), state, 0, 0, 0, StrategyType.CONTAINED); int itemCount = 10; Map positionMap = stateList.getKeylineStateForPositionMap( @@ -402,8 +409,14 @@ public void testKeylineStateForPositionVertical() { .addKeylineRange(100F, 0F, 100F, 4, true) .addKeyline(475F, .5F, 50F) .build(); - KeylineStateList stateList = KeylineStateList.from( - createCarouselWithSizeAndOrientation(500, CarouselLayoutManager.VERTICAL), state, 0, 0, 0); + KeylineStateList stateList = + KeylineStateList.from( + createCarouselWithSizeAndOrientation(500, CarouselLayoutManager.VERTICAL), + state, + 0, + 0, + 0, + StrategyType.CONTAINED); int itemCount = 10; Map positionMap = stateList.getKeylineStateForPositionMap( itemCount, 0, 1000, false); @@ -443,7 +456,7 @@ public void testCutoffEndKeylines_changeEndKeylineLocOffsets() { .addAnchorKeyline(125F, getKeylineMaskPercentage(10F, 40F), 10F) .build(); KeylineStateList stateList = - KeylineStateList.from(createCarouselWithWidth(100), state, 0, 0, 0); + KeylineStateList.from(createCarouselWithWidth(100), state, 0, 0, 0, StrategyType.CONTAINED); float[] scrollSteps = new float[] {40F, 0F}; float minScroll = 0F; @@ -477,7 +490,7 @@ public void testCutoffStartKeylines_doesNotChangeEndKeylineLocOffsets() { .addAnchorKeyline(105F, getKeylineMaskPercentage(10F, 40F), 10F) .build(); KeylineStateList stateList = - KeylineStateList.from(createCarouselWithWidth(100), state, 0, 0, 0); + KeylineStateList.from(createCarouselWithWidth(100), state, 0, 0, 0, StrategyType.CONTAINED); float[] scrollSteps = new float[] {40F, 0F}; float minScroll = 0F; @@ -492,7 +505,7 @@ public void testCutoffStartKeylines_doesNotChangeEndKeylineLocOffsets() { } @Test - public void testStartPadding_shiftsStartState() { + public void testStartPadding_shiftsContainedStartState() { // Default state: [small, large, small] where small is 20F and large is 60F KeylineState state = new KeylineState.Builder(60F, 100) @@ -509,7 +522,8 @@ public void testStartPadding_shiftsStartState() { state, /* itemMargins= */ 0, /* leftOrTopPadding= */ 12, - /* rightOrBottomPadding= */20); + /* rightOrBottomPadding= */20, + /* strategyType= */ StrategyType.CONTAINED); // Normally start state is expected to have locOffests of [30F, 70F, 90F] but with a start // padding of 12, it should be evenly decreased from all items. So the first item should start @@ -523,7 +537,7 @@ public void testStartPadding_shiftsStartState() { } @Test - public void testEndPadding_shiftsEndState() { + public void testEndPadding_shiftsContainedEndState() { // Default state: [small, large, small] where small is 20F and large is 60F KeylineState state = new KeylineState.Builder(60F, 100) @@ -540,7 +554,8 @@ public void testEndPadding_shiftsEndState() { state, /* itemMargins= */ 0, /* leftOrTopPadding= */ 12, - /* rightOrBottomPadding= */24); + /* rightOrBottomPadding= */ 24, + /* strategyType= */ StrategyType.CONTAINED); // Normally start state is expected to have locOffests of [10F, 30F, 70F] but with an end // padding of 24, it should be evenly decreased from all items. @@ -551,4 +566,69 @@ public void testEndPadding_shiftsEndState() { assertThat(actual.get(i).locOffset).isEqualTo(locOffsets[i]); } } + + @Test + public void testStartPadding_shiftsUncontainedStartState() { + // Default state: [small, large, small] where small is 20F and large is 60F + KeylineState state = + new KeylineState.Builder(60F, 100) + .addAnchorKeyline(-5F, getKeylineMaskPercentage(10F, 60F), 10F) + .addKeyline(10F, getKeylineMaskPercentage(20F, 60F), 20F) + .addKeyline(50F, 0F, 60F, true) + .addKeyline(90F, getKeylineMaskPercentage(20F, 60F), 20F) + .addAnchorKeyline(105F, getKeylineMaskPercentage(10F, 60F), 10F) + .build(); + Carousel carousel = createCarouselWithWidth(100); + KeylineStateList stateList = + KeylineStateList.from( + carousel, + state, + /* itemMargins= */ 0, + /* leftOrTopPadding= */ 12, + /* rightOrBottomPadding= */20, + /* strategyType= */ StrategyType.UNCONTAINED); + + // Normally start state is expected to have locOffests of [30F, 70F, 90F]. Shift by left padding + float[] locOffsets = new float[] {-5F, 42F, 82F, 102F, 117F}; + float[] cutoffs = new float[] {10F, 0F, 0F, 12F, 22F}; + + List actual = stateList.getStartState().getKeylines(); + for (int i = 0; i < actual.size(); i++) { + assertThat(actual.get(i).locOffset).isEqualTo(locOffsets[i]); + assertThat(actual.get(i).cutoff).isEqualTo(cutoffs[i]); + } + } + + @Test + public void testEndPadding_shiftsUncontainedEndState() { + // Default state: [small, large, small] where small is 20F and large is 60F + KeylineState state = + new KeylineState.Builder(60F, 100) + .addAnchorKeyline(-5F, getKeylineMaskPercentage(10F, 60F), 10F) + .addKeyline(10F, getKeylineMaskPercentage(20F, 60F), 20F) + .addKeyline(50F, 0F, 60F, true) + .addKeyline(90F, getKeylineMaskPercentage(20F, 60F), 20F) + .addAnchorKeyline(105F, getKeylineMaskPercentage(10F, 60F), 10F) + .build(); + Carousel carousel = createCarouselWithWidth(100); + KeylineStateList stateList = + KeylineStateList.from( + carousel, + state, + /* itemMargins= */ 0, + /* leftOrTopPadding= */ 12, + /* rightOrBottomPadding= */ 24, + /* strategyType= */ StrategyType.UNCONTAINED); + + // Normally end state is expected to have locOffsets of [10F, 30F, 70F]. Shift by right + // padding + float[] locOffsets = new float[] {-29F, -14F, 6F, 46F, 105F}; + float[] cutoffs = new float[] {34F, 24F, 4F, 0F, 10F}; + + List actual = stateList.getEndState().getKeylines(); + for (int i = 0; i < actual.size(); i++) { + assertThat(actual.get(i).locOffset).isEqualTo(locOffsets[i]); + assertThat(actual.get(i).cutoff).isEqualTo(cutoffs[i]); + } + } }