Skip to content

Commit 9393b97

Browse files
committed
[Carousel] Support unclipped padding for uncontained variant of carousel
PiperOrigin-RevId: 625101250
1 parent d056cc3 commit 9393b97

File tree

9 files changed

+252
-93
lines changed

9 files changed

+252
-93
lines changed

catalog/java/io/material/catalog/carousel/res/layout/cat_carousel_uncontained_fragment.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@
112112
android:id="@+id/uncontained_carousel_recycler_view"
113113
android:layout_width="match_parent"
114114
android:layout_height="196dp"
115-
android:layout_marginHorizontal="16dp"
115+
android:paddingStart="16dp"
116+
android:paddingEnd="16dp"
116117
android:layout_marginVertical="16dp"
117118
android:clipChildren="false"
118119
android:clipToPadding="false" />

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

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import androidx.core.math.MathUtils;
5656
import androidx.core.util.Preconditions;
5757
import androidx.core.view.ViewCompat;
58+
import com.google.android.material.carousel.CarouselStrategy.StrategyType;
5859
import com.google.android.material.carousel.KeylineState.Keyline;
5960
import java.lang.annotation.Retention;
6061
import java.lang.annotation.RetentionPolicy;
@@ -98,6 +99,7 @@ public class CarouselLayoutManager extends LayoutManager
9899
private boolean isDebuggingEnabled = false;
99100
private final DebugItemDecoration debugItemDecoration = new DebugItemDecoration();
100101
@NonNull private CarouselStrategy carouselStrategy;
102+
101103
@Nullable private KeylineStateList keylineStateList;
102104
// A KeylineState shifted for any current scroll offset.
103105
@Nullable private KeylineState currentKeylineState;
@@ -227,8 +229,7 @@ public int getCarouselAlignment() {
227229
private int getLeftOrTopPaddingForKeylineShift() {
228230
// TODO(b/316969331): Fix keyline shifting by decreasing carousel size when carousel is clipped
229231
// to padding.
230-
// TODO(b/316968490): Fix keyline shifting by adjusting cutoffs if strategy is not contained.
231-
if (getClipToPadding() || !carouselStrategy.isContained()) {
232+
if (getClipToPadding()) {
232233
return 0;
233234
}
234235
if (getOrientation() == VERTICAL) {
@@ -240,8 +241,7 @@ private int getLeftOrTopPaddingForKeylineShift() {
240241
private int getRightOrBottomPaddingForKeylineShift() {
241242
// TODO(b/316969331): Fix keyline shifting by decreasing carousel size when carousel is clipped
242243
// to padding.
243-
// TODO(b/316968490): Fix keyline shifting by adjusting cutoffs if strategy is not contained.
244-
if (getClipToPadding() || !carouselStrategy.isContained()) {
244+
if (getClipToPadding()) {
245245
return 0;
246246
}
247247
if (getOrientation() == VERTICAL) {
@@ -348,7 +348,8 @@ private void recalculateKeylineStateList(Recycler recycler) {
348348
isLayoutRtl() ? KeylineState.reverse(keylineState, getContainerSize()) : keylineState,
349349
getItemMargins(),
350350
getLeftOrTopPaddingForKeylineShift(),
351-
getRightOrBottomPaddingForKeylineShift());
351+
getRightOrBottomPaddingForKeylineShift(),
352+
carouselStrategy.getStrategyType());
352353
}
353354

354355
private int getItemMargins() {
@@ -853,10 +854,7 @@ private int calculateEndScroll(State state, KeylineStateList stateList) {
853854
float lastItemDistanceFromFirstItem =
854855
((state.getItemCount() - 1) * endState.getItemSize()) * (isRtl ? -1F : 1F);
855856

856-
float endPadding =
857-
isRtl ? -endFocalKeyline.leftOrTopPaddingShift : endFocalKeyline.rightOrBottomPaddingShift;
858857
float endFocalLocDistanceFromStart = endFocalKeyline.loc - getParentStart();
859-
float endFocalLocDistanceFromEnd = getParentEnd() - endFocalKeyline.loc;
860858

861859
// We want the last item in the list to only be able to scroll to the end of the list. Subtract
862860
// 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) {
865863
(int)
866864
(lastItemDistanceFromFirstItem
867865
- endFocalLocDistanceFromStart
868-
+ endFocalLocDistanceFromEnd
869-
// If there is padding, adjust for the extra padding offset since offset is
870-
// implicitly added from both endFocalLocDistance calculations.
871-
+ endPadding);
872-
866+
+ (isRtl ? -1 : 1) * endFocalKeyline.maskedItemSize / 2F);
873867
return isRtl ? min(0, endScroll) : max(0, endScroll);
874868
}
875869

@@ -994,7 +988,7 @@ private void updateChildMaskForLocation(
994988
// container instead of bleeding and being clipped by the RecyclerView's bounds.
995989
// Only do this if there is only one side of the mask that is out of bounds; if
996990
// both sides are out of bounds on the same side, then the whole mask is out of view.
997-
if (carouselStrategy.isContained()) {
991+
if (carouselStrategy.getStrategyType() == StrategyType.CONTAINED) {
998992
orientationHelper.containMaskWithinBounds(maskRect, offsetMaskRect, parentBoundsRect);
999993
}
1000994

@@ -1060,10 +1054,6 @@ private int getParentRight() {
10601054
return orientationHelper.getParentRight();
10611055
}
10621056

1063-
private int getParentEnd() {
1064-
return orientationHelper.getParentEnd();
1065-
}
1066-
10671057
private int getParentTop() {
10681058
return orientationHelper.getParentTop();
10691059
}

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ public abstract class CarouselStrategy {
3131

3232
private float smallSizeMax;
3333

34+
/**
35+
* Enum that defines whether or not the strategy is contained or uncontained. Contained strategies
36+
* will always have all of its items within bounds of the carousel width.
37+
*/
38+
enum StrategyType {
39+
CONTAINED,
40+
UNCONTAINED
41+
}
42+
3443
void initialize(Context context) {
3544
smallSizeMin =
3645
smallSizeMin > 0 ? smallSizeMin : CarouselStrategyHelper.getSmallSizeMin(context);
@@ -130,14 +139,16 @@ static int[] doubleCounts(int[] count) {
130139
}
131140

132141
/**
133-
* Gets whether this carousel should mask items against the edges of the carousel container.
142+
* Gets the strategy type of this strategy. Contained strategies should mask items against the
143+
* edges of the carousel container.
134144
*
135-
* @return true if items in the carousel should mask/squash against the edges of the carousel
136-
* container. false if the carousel should allow items to bleed past the edges of the
137-
* container and be clipped.
145+
* @return the {@link StrategyType} of this strategy. A value of {@link StrategyType#CONTAINED}
146+
* means items in the carousel should mask/squash against the edges of the carousel container.
147+
* {@link StrategyType#UNCONTAINED} means the carousel should allow items to bleed past the edges
148+
* of the container and be clipped.
138149
*/
139-
boolean isContained() {
140-
return true;
150+
StrategyType getStrategyType() {
151+
return StrategyType.CONTAINED;
141152
}
142153

143154
/**

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

Lines changed: 88 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

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

19+
import static java.lang.Math.max;
20+
import static java.lang.Math.min;
21+
1922
import androidx.annotation.NonNull;
2023
import androidx.core.math.MathUtils;
2124
import com.google.android.material.animation.AnimationUtils;
@@ -82,11 +85,17 @@ private KeylineStateList(
8285
}
8386

8487
/** Creates a new {@link KeylineStateList} from a {@link KeylineState}. */
85-
static KeylineStateList from(Carousel carousel, KeylineState state, float itemMargins,
86-
float leftOrTopPadding, float rightOrBottomPadding) {
88+
static KeylineStateList from(
89+
Carousel carousel,
90+
KeylineState state,
91+
float itemMargins,
92+
float leftOrTopPadding,
93+
float rightOrBottomPadding,
94+
CarouselStrategy.StrategyType strategyType) {
8795
return new KeylineStateList(
88-
state, getStateStepsStart(carousel, state, itemMargins, leftOrTopPadding),
89-
getStateStepsEnd(carousel, state, itemMargins, rightOrBottomPadding));
96+
state,
97+
getStateStepsStart(carousel, state, itemMargins, leftOrTopPadding, strategyType),
98+
getStateStepsEnd(carousel, state, itemMargins, rightOrBottomPadding, strategyType));
9099
}
91100

92101
/** Returns the default state for this state list. */
@@ -155,14 +164,17 @@ KeylineState getShiftedState(
155164
float startShiftOffset = minScrollOffset + startShiftRange;
156165
float endShiftOffset = maxScrollOffset - endShiftRange;
157166
float startPaddingShift = getStartState().getFirstFocalKeyline().leftOrTopPaddingShift;
158-
float endPaddingShift = getEndState().getLastFocalKeyline().rightOrBottomPaddingShift;
167+
float endPaddingShift = getEndState().getFirstFocalKeyline().rightOrBottomPaddingShift;
159168

160169
// Normally we calculate the interpolation such that by scrollShiftOffset, we are always at the
161-
// default state but in this case, we want to start shifting earlier/increase startShiftOffset
162-
// so that the interpolation will choose the start state instead of the default state when the
163-
// scroll offset is equal to startPaddingShift. This is so we are always at the start state
164-
// at the beginning of the carousel, instead of getting to a state where the start state is only
170+
// default state. In the case where the start state is equal to the default state but with
171+
// padding, we want to start shifting earlier/increase startShiftOffset so that the
172+
// interpolation will choose the start state instead of the default state when the scroll offset
173+
// is equal to startPaddingShift. This is so we are always at the start state with padding at
174+
// the beginning of the carousel, instead of getting to a state where the start state is only
165175
// when scrollOffset <= startPaddingShift.
176+
// We know that the start state is equal to the default state with padding if the start shift
177+
// range is equal to the padding.
166178
if (startShiftRange == startPaddingShift) {
167179
startShiftOffset += startPaddingShift;
168180
}
@@ -362,7 +374,56 @@ private static boolean isLastFocalItemVisibleAtRightOfContainer(
362374
&& state.getLastFocalKeyline() == state.getLastNonAnchorKeyline();
363375
}
364376

377+
@NonNull
365378
private static KeylineState shiftKeylineStateForPadding(
379+
@NonNull KeylineState keylineState, float padding, float carouselSize, boolean leftShift,
380+
float childMargins, CarouselStrategy.StrategyType strategyType) {
381+
switch (strategyType) {
382+
case CONTAINED:
383+
return shiftKeylineStateForPaddingContained(
384+
keylineState, padding, carouselSize, leftShift, childMargins);
385+
default:
386+
return shiftKeylineStateForPaddingUncontained(
387+
keylineState, padding, carouselSize, leftShift);
388+
}
389+
}
390+
391+
@NonNull
392+
private static KeylineState shiftKeylineStateForPaddingUncontained(
393+
@NonNull KeylineState keylineState, float padding, float carouselSize, boolean leftShift) {
394+
List<Keyline> tmpKeylines = new ArrayList<>(keylineState.getKeylines());
395+
KeylineState.Builder builder =
396+
new KeylineState.Builder(keylineState.getItemSize(), carouselSize);
397+
int unchangingAnchorPosition = leftShift ? 0 : tmpKeylines.size() - 1;
398+
for (int j = 0; j < tmpKeylines.size(); j++) {
399+
Keyline k = tmpKeylines.get(j);
400+
if (k.isAnchor && j == unchangingAnchorPosition) {
401+
builder.addKeyline(k.locOffset, k.mask, k.maskedItemSize, false, true, k.cutoff);
402+
continue;
403+
}
404+
float newOffset = leftShift ? k.locOffset + padding : k.locOffset - padding;
405+
float leftOrTopPadding = leftShift ? padding : 0;
406+
float rightOrBottomPadding = leftShift ? 0 : padding;
407+
boolean isFocal =
408+
j >= keylineState.getFirstFocalKeylineIndex()
409+
&& j <= keylineState.getLastFocalKeylineIndex();
410+
builder.addKeyline(
411+
newOffset,
412+
k.mask,
413+
k.maskedItemSize,
414+
isFocal,
415+
k.isAnchor,
416+
Math.abs(
417+
leftShift
418+
? max(0, newOffset + k.maskedItemSize / 2 - carouselSize)
419+
: min(0, newOffset - k.maskedItemSize / 2)),
420+
leftOrTopPadding,
421+
rightOrBottomPadding);
422+
}
423+
return builder.build();
424+
}
425+
426+
private static KeylineState shiftKeylineStateForPaddingContained(
366427
KeylineState keylineState, float padding, float carouselSize, boolean leftShift,
367428
float childMargins) {
368429

@@ -400,7 +461,7 @@ private static KeylineState shiftKeylineStateForPadding(
400461
maskedItemSize, keylineState.getItemSize(), childMargins);
401462
float locOffset = nextOffset + maskedItemSize / 2F;
402463

403-
float actualPaddingShift = locOffset - k.locOffset;
464+
float actualPaddingShift = Math.abs(locOffset - k.locOffset);
404465

405466
builder.addKeyline(
406467
locOffset,
@@ -434,7 +495,7 @@ private static KeylineState shiftKeylineStateForPadding(
434495
*/
435496
private static List<KeylineState> getStateStepsStart(
436497
Carousel carousel, KeylineState defaultState, float itemMargins,
437-
float leftOrTopPaddingForKeylineShift) {
498+
float leftOrTopPaddingForKeylineShift, CarouselStrategy.StrategyType strategyType) {
438499
List<KeylineState> steps = new ArrayList<>();
439500
steps.add(defaultState);
440501
int firstNonAnchorKeylineIndex = findFirstNonAnchorKeylineIndex(defaultState);
@@ -453,7 +514,8 @@ private static List<KeylineState> getStateStepsStart(
453514
leftOrTopPaddingForKeylineShift,
454515
carouselSize,
455516
true,
456-
itemMargins));
517+
itemMargins,
518+
strategyType));
457519
}
458520
return steps;
459521
}
@@ -471,8 +533,10 @@ private static List<KeylineState> getStateStepsStart(
471533
// view.
472534
float cutoffs = defaultState.getFirstFocalKeyline().cutoff;
473535
steps.add(
474-
shiftKeylinesAndCreateKeylineState(defaultState, originalStart + cutoffs, carouselSize));
475-
// TODO(b/316968490): If there is padding, this should affect keylines and the cutoffs
536+
shiftKeylinesAndCreateKeylineState(
537+
defaultState,
538+
originalStart + cutoffs + leftOrTopPaddingForKeylineShift,
539+
carouselSize));
476540
return steps;
477541
}
478542

@@ -512,7 +576,8 @@ private static List<KeylineState> getStateStepsStart(
512576
leftOrTopPaddingForKeylineShift,
513577
carouselSize,
514578
true,
515-
itemMargins);
579+
itemMargins,
580+
strategyType);
516581
}
517582
steps.add(shifted);
518583
}
@@ -536,7 +601,8 @@ private static List<KeylineState> getStateStepsStart(
536601
* carousel.
537602
*/
538603
private static List<KeylineState> getStateStepsEnd(Carousel carousel, KeylineState defaultState,
539-
float itemMargins, float rightOrBottomPaddingForKeylineShift) {
604+
float itemMargins, float rightOrBottomPaddingForKeylineShift,
605+
CarouselStrategy.StrategyType strategyType) {
540606
List<KeylineState> steps = new ArrayList<>();
541607
steps.add(defaultState);
542608
int lastNonAnchorKeylineIndex = findLastNonAnchorKeylineIndex(defaultState);
@@ -556,7 +622,8 @@ private static List<KeylineState> getStateStepsEnd(Carousel carousel, KeylineSta
556622
rightOrBottomPaddingForKeylineShift,
557623
carouselSize,
558624
false,
559-
itemMargins));
625+
itemMargins,
626+
strategyType));
560627
}
561628
return steps;
562629
}
@@ -573,9 +640,8 @@ private static List<KeylineState> getStateStepsEnd(Carousel carousel, KeylineSta
573640
// view. Add a step that shifts all the keylines over to bring the last focal item into full
574641
// view.
575642
float cutoffs = defaultState.getLastFocalKeyline().cutoff;
576-
steps.add(
577-
shiftKeylinesAndCreateKeylineState(defaultState, originalStart - cutoffs, carouselSize));
578-
// TODO(b/316968490): If there is padding, this should affect keylines and the cutoffs
643+
steps.add(shiftKeylinesAndCreateKeylineState(defaultState,
644+
originalStart - cutoffs - rightOrBottomPaddingForKeylineShift, carouselSize));
579645
return steps;
580646
}
581647

@@ -614,7 +680,8 @@ private static List<KeylineState> getStateStepsEnd(Carousel carousel, KeylineSta
614680
rightOrBottomPaddingForKeylineShift,
615681
carouselSize,
616682
false,
617-
itemMargins);
683+
itemMargins,
684+
strategyType);
618685
}
619686
steps.add(shifted);
620687
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ private KeylineState createLeftAlignedKeylineState(
237237
}
238238

239239
@Override
240-
boolean isContained() {
241-
return false;
240+
StrategyType getStrategyType() {
241+
return StrategyType.UNCONTAINED;
242242
}
243243
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import androidx.test.core.app.ApplicationProvider;
4141
import com.google.android.material.carousel.CarouselHelper.CarouselTestAdapter;
4242
import com.google.android.material.carousel.CarouselHelper.WrappedCarouselLayoutManager;
43+
import com.google.android.material.carousel.CarouselStrategy.StrategyType;
4344
import org.junit.Before;
4445
import org.junit.Test;
4546
import org.junit.runner.RunWith;
@@ -100,7 +101,7 @@ public void testScrollBeyondMaxHorizontalScroll_shouldLimitToMaxScrollOffset() t
100101
KeylineState leftState =
101102
KeylineStateList.from(
102103
layoutManager, KeylineState.reverse(keylineState, DEFAULT_RECYCLER_VIEW_WIDTH),
103-
0, 0, 0)
104+
0, 0, 0, StrategyType.CONTAINED)
104105
.getStartState();
105106

106107
MaskableFrameLayout child =

0 commit comments

Comments
 (0)