Skip to content

Commit 85b6d50

Browse files
hunterstichimhappi
authored andcommittedMay 22, 2023
[Carousel] Fixed multi browse strategy clipping extra small items before being fully collapsed
This moves mask rect calculation from MaskableFrameLayout into CarouselLayoutManager so CarouselLayoutManager can change the offsetting of the mask inside a child and clip according to both the keylines and the carousel container boundary. PiperOrigin-RevId: 533082558
1 parent 0bcb570 commit 85b6d50

File tree

6 files changed

+223
-58
lines changed

6 files changed

+223
-58
lines changed
 

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

+76-42
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
import static com.google.android.material.animation.AnimationUtils.lerp;
2222
import static java.lang.Math.abs;
2323
import static java.lang.Math.max;
24+
import static java.lang.Math.min;
Has a conversation. Original line has a conversation.
2425

2526
import android.graphics.Canvas;
2627
import android.graphics.Color;
2728
import android.graphics.Paint;
2829
import android.graphics.PointF;
2930
import android.graphics.Rect;
31+
import android.graphics.RectF;
3032
import androidx.recyclerview.widget.LinearSmoothScroller;
3133
import androidx.recyclerview.widget.RecyclerView;
3234
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
@@ -94,21 +96,25 @@ public class CarouselLayoutManager extends LayoutManager
9496
* RecyclerView and laid out.
9597
*/
9698
private static final class ChildCalculations {
97-
View child;
98-
float locOffset;
99-
KeylineRange range;
99+
final View child;
100+
final float center;
101+
final float offsetCenter;
102+
final KeylineRange range;
100103

101104
/**
102105
* Creates new calculations object.
103106
*
104107
* @param child The child being calculated for
105-
* @param locOffset the offset location along the scrolling axis where this child will be laid
106-
* out
108+
* @param center the location of the center of the {@code child} along the scrolling axis in the
109+
* end-to-end model
110+
* @param offsetCenter the offset location of the center of the {@code child} along the
111+
* scrolling axis where this child will be laid out
107112
* @param range the keyline range that surrounds {@code locOffset}
108113
*/
109-
ChildCalculations(View child, float locOffset, KeylineRange range) {
114+
ChildCalculations(View child, float center, float offsetCenter, KeylineRange range) {
110115
this.child = child;
111-
this.locOffset = locOffset;
116+
this.center = center;
117+
this.offsetCenter = offsetCenter;
112118
this.range = range;
113119
}
114120
}
@@ -250,18 +256,18 @@ private void addViewsStart(Recycler recycler, int startPosition) {
250256
int start = calculateChildStartForFill(startPosition);
251257
for (int i = startPosition; i >= 0; i--) {
252258
ChildCalculations calculations = makeChildCalculations(recycler, start, i);
253-
if (isLocOffsetOutOfFillBoundsStart(calculations.locOffset, calculations.range)) {
259+
if (isLocOffsetOutOfFillBoundsStart(calculations.offsetCenter, calculations.range)) {
254260
break;
255261
}
256262
start = addStart(start, (int) currentKeylineState.getItemSize());
257263

258264
// If this child's start is beyond the end of the container, don't add the child but continue
259265
// to loop so we can eventually get to children that are within bounds.
260-
if (isLocOffsetOutOfFillBoundsEnd(calculations.locOffset, calculations.range)) {
266+
if (isLocOffsetOutOfFillBoundsEnd(calculations.offsetCenter, calculations.range)) {
261267
continue;
262268
}
263269
// Add this child to the first index of the RecyclerView.
264-
addAndLayoutView(calculations.child, /* index= */ 0, calculations.locOffset);
270+
addAndLayoutView(calculations.child, /* index= */ 0, calculations);
265271
}
266272
}
267273

@@ -277,18 +283,18 @@ private void addViewsEnd(Recycler recycler, State state, int startPosition) {
277283
int start = calculateChildStartForFill(startPosition);
278284
for (int i = startPosition; i < state.getItemCount(); i++) {
279285
ChildCalculations calculations = makeChildCalculations(recycler, start, i);
280-
if (isLocOffsetOutOfFillBoundsEnd(calculations.locOffset, calculations.range)) {
286+
if (isLocOffsetOutOfFillBoundsEnd(calculations.offsetCenter, calculations.range)) {
281287
break;
282288
}
283289
start = addEnd(start, (int) currentKeylineState.getItemSize());
284290

285291
// If this child's end is beyond the start of the container, don't add the child but continue
286292
// to loop so we can eventually get to children that are within bounds.
287-
if (isLocOffsetOutOfFillBoundsStart(calculations.locOffset, calculations.range)) {
293+
if (isLocOffsetOutOfFillBoundsStart(calculations.offsetCenter, calculations.range)) {
288294
continue;
289295
}
290296
// Add this child to the last index of the RecyclerView
291-
addAndLayoutView(calculations.child, /* index= */ -1, calculations.locOffset);
297+
addAndLayoutView(calculations.child, /* index= */ -1, calculations);
292298
}
293299
}
294300

@@ -359,14 +365,12 @@ private ChildCalculations makeChildCalculations(Recycler recycler, float start,
359365
View child = recycler.getViewForPosition(position);
360366
measureChildWithMargins(child, 0, 0);
361367

362-
int centerX = addEnd((int) start, (int) halfItemSize);
368+
int center = addEnd((int) start, (int) halfItemSize);
363369
KeylineRange range =
364-
getSurroundingKeylineRange(currentKeylineState.getKeylines(), centerX, false);
365-
366-
float offsetCx = calculateChildOffsetCenterForLocation(child, centerX, range);
367-
updateChildMaskForLocation(child, centerX, range);
370+
getSurroundingKeylineRange(currentKeylineState.getKeylines(), center, false);
368371

369-
return new ChildCalculations(child, offsetCx, range);
372+
float offsetCenter = calculateChildOffsetCenterForLocation(child, center, range);
373+
return new ChildCalculations(child, center, offsetCenter, range);
370374
}
371375

372376
/**
@@ -376,17 +380,18 @@ private ChildCalculations makeChildCalculations(Recycler recycler, float start,
376380
* @param child the child view to add and lay out
377381
* @param index the index at which to add the child to the RecyclerView. Use 0 for adding to the
378382
* start of the list and -1 for adding to the end.
379-
* @param offsetCx where the center of the masked child should be placed along the scrolling axis
383+
* @param calculations the child calculations to be used to layout this view
380384
*/
381-
private void addAndLayoutView(View child, int index, float offsetCx) {
385+
private void addAndLayoutView(View child, int index, ChildCalculations calculations) {
382386
float halfItemSize = currentKeylineState.getItemSize() / 2F;
383387
addView(child, index);
384388
layoutDecoratedWithMargins(
385389
child,
386-
/* left= */ (int) (offsetCx - halfItemSize),
390+
/* left= */ (int) (calculations.offsetCenter - halfItemSize),
387391
/* top= */ getParentTop(),
388-
/* right= */ (int) (offsetCx + halfItemSize),
392+
/* right= */ (int) (calculations.offsetCenter + halfItemSize),
389393
/* bottom= */ getParentBottom());
394+
updateChildMaskForLocation(child, calculations.center, calculations.range);
390395
}
391396

392397
/**
@@ -745,18 +750,42 @@ private float getMaskedItemSizeForLocOffset(float locOffset, KeylineRange range)
745750
*/
746751
private void updateChildMaskForLocation(
747752
View child, float childCenterLocation, KeylineRange range) {
748-
if (child instanceof Maskable) {
749-
// Interpolate the mask value based on the location of this view between it's two
750-
// surrounding keylines.
751-
float maskProgress =
752-
lerp(
753-
range.left.mask,
754-
range.right.mask,
755-
range.left.loc,
756-
range.right.loc,
757-
childCenterLocation);
758-
((Maskable) child).setMaskXPercentage(maskProgress);
753+
if (!(child instanceof Maskable)) {
754+
return;
759755
}
756+
757+
// Interpolate the mask value based on the location of this view between it's two
758+
// surrounding keylines.
759+
float maskProgress =
760+
lerp(
761+
range.left.mask,
762+
range.right.mask,
763+
range.left.loc,
764+
range.right.loc,
765+
childCenterLocation);
766+
767+
float childHeight = child.getHeight();
768+
float childWidth = child.getWidth();
769+
// Translate the percentage into an actual pixel value of how much of this view should be
770+
// masked away.
771+
float maskWidth = lerp(0F, childWidth / 2F, 0F, 1F, maskProgress);
772+
RectF maskRect = new RectF(maskWidth, 0F, (childWidth - maskWidth), childHeight);
773+
774+
// If the carousel is a CONTAINED carousel, ensure the mask collapses against the side of the
775+
// container instead of bleeding and being clipped by the RecyclerView's bounds.
776+
if (carouselStrategy.isContained()) {
777+
float offsetCx = calculateChildOffsetCenterForLocation(child, childCenterLocation, range);
778+
float maskedLeft = offsetCx - (maskRect.width() / 2F);
779+
float maskedRight = offsetCx + (maskRect.width() / 2F);
780+
781+
if (maskedLeft < getParentLeft()) {
782+
maskRect.left = min(maskRect.left + (getParentLeft() - maskedLeft), childWidth / 2F);
783+
}
784+
if (maskedRight > getParentRight()) {
785+
maskRect.right = max(maskRect.right - (maskedRight - getParentRight()), childWidth / 2F);
786+
}
787+
}
788+
((Maskable) child).setMaskRectF(maskRect);
760789
}
761790

762791
@Override
@@ -797,12 +826,20 @@ public void measureChildWithMargins(@NonNull View child, int widthUsed, int heig
797826
child.measure(widthSpec, heightSpec);
798827
}
799828

829+
private int getParentLeft() {
830+
return 0;
831+
}
832+
800833
private int getParentStart() {
801-
return isLayoutRtl() ? getWidth() : 0;
834+
return isLayoutRtl() ? getParentRight() : getParentLeft();
835+
}
836+
837+
private int getParentRight() {
838+
return getWidth();
802839
}
803840

804841
private int getParentEnd() {
805-
return isLayoutRtl() ? 0 : getWidth();
842+
return isLayoutRtl() ? getParentLeft() : getParentRight();
806843
}
807844

808845
private int getParentTop() {
@@ -890,8 +927,7 @@ public void scrollToPosition(int position) {
890927
if (keylineStateList == null) {
891928
return;
892929
}
893-
horizontalScrollOffset =
894-
getScrollOffsetForPosition(position);
930+
horizontalScrollOffset = getScrollOffsetForPosition(position);
895931
currentFillStartPosition = MathUtils.clamp(position, 0, max(0, getItemCount() - 1));
896932
updateCurrentKeylineStateForScrollOffset();
897933
requestLayout();
@@ -911,8 +947,7 @@ public PointF computeScrollVectorForPosition(int targetPosition) {
911947
public int calculateDxToMakeVisible(View view, int snapPreference) {
912948
// Override dx calculations so the target view is brought all the way into the focal
913949
// range instead of just being made visible.
914-
float targetScrollOffset =
915-
getScrollOffsetForPosition(getPosition(view));
950+
float targetScrollOffset = getScrollOffsetForPosition(getPosition(view));
916951
return (int) (horizontalScrollOffset - targetScrollOffset);
917952
}
918953
};
@@ -1006,13 +1041,12 @@ private void offsetChildLeftAndRight(
10061041
int centerX = addEnd((int) startOffset, (int) halfItemSize);
10071042
KeylineRange range =
10081043
getSurroundingKeylineRange(currentKeylineState.getKeylines(), centerX, false);
1009-
10101044
float offsetCx = calculateChildOffsetCenterForLocation(child, centerX, range);
1011-
updateChildMaskForLocation(child, centerX, range);
10121045

10131046
// Offset the child so its center is at offsetCx
10141047
super.getDecoratedBoundsWithMargins(child, boundsRect);
10151048
float actualCx = boundsRect.left + halfItemSize;
1049+
updateChildMaskForLocation(child, centerX, range);
10161050
child.offsetLeftAndRight((int) (offsetCx - actualCx));
10171051
}
10181052

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

+11
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,15 @@ abstract KeylineState onFirstChildMeasuredWithMargins(
102102
static float getChildMaskPercentage(float maskedSize, float unmaskedSize, float childMargins) {
103103
return 1F - ((maskedSize - childMargins) / (unmaskedSize - childMargins));
104104
}
105+
106+
/**
107+
* Gets whether this carousel should mask items against the edges of the carousel container.
108+
*
109+
* @return true if items in the carousel should mask/squash against the edges of the carousel
110+
* container. false if the carousel should allow items to bleed past the edges of the
111+
* container and be clipped.
112+
*/
113+
boolean isContained() {
114+
return true;
115+
}
105116
}

‎lib/java/com/google/android/material/carousel/Maskable.java

+10
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ interface Maskable {
2828
/**
2929
* Set the percentage by which this {@link View} should mask itself along the x axis.
3030
*
31+
* <p>This method serves the same purpose as {@link #setMaskRectF(RectF)} but requires the
32+
* implementing view to calculate the correct rect given the mask percentage.
33+
*
3134
* @param percentage 0 when this view is fully unmasked. 1 when this view is fully masked.
3235
*/
3336
void setMaskXPercentage(@FloatRange(from = 0F, to = 1F) float percentage);
@@ -40,6 +43,13 @@ interface Maskable {
4043
@FloatRange(from = 0F, to = 1F)
4144
float getMaskXPercentage();
4245

46+
/**
47+
* Sets a {@link RectF} that this {@link View} will mask itself by.
48+
*
49+
* @param maskRect a rect in the view's coordinates to mask by
50+
*/
51+
void setMaskRectF(@NonNull RectF maskRect);
52+
4353
/** Gets a {@link RectF} that this {@link View} is masking itself by. */
4454
@NonNull
4555
RectF getMaskRectF();

‎lib/java/com/google/android/material/carousel/MaskableFrameLayout.java

+15-5
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,24 @@ public void setMaskXPercentage(float percentage) {
120120
percentage = MathUtils.clamp(percentage, 0F, 1F);
121121
if (maskXPercentage != percentage) {
122122
this.maskXPercentage = percentage;
123-
onMaskChanged();
123+
// Translate the percentage into an actual pixel value of how much of this view should be
124+
// masked away.
125+
float maskWidth = AnimationUtils.lerp(0f, getWidth() / 2F, 0f, 1f, maskXPercentage);
126+
setMaskRectF(new RectF(maskWidth, 0F, (getWidth() - maskWidth), getHeight()));
124127
}
125128
}
126129

130+
/**
131+
* Sets the {@link RectF} that this {@link View} will be masked by.
132+
*
133+
* @param maskRect a rect in the view's coordinates to mask by
134+
*/
135+
@Override
136+
public void setMaskRectF(@NonNull RectF maskRect) {
137+
this.maskRect.set(maskRect);
138+
onMaskChanged();
139+
}
140+
127141
/**
128142
* Gets the percentage by which this {@link View} is masked by along the x axis.
129143
*
@@ -150,10 +164,6 @@ private void onMaskChanged() {
150164
if (getWidth() == 0) {
151165
return;
152166
}
153-
// Translate the percentage into an actual pixel value of how much of this view should be
154-
// masked away.
155-
float maskWidth = AnimationUtils.lerp(0f, getWidth() / 2F, 0f, 1f, maskXPercentage);
156-
maskRect.set(maskWidth, 0F, (getWidth() - maskWidth), getHeight());
157167
shapeableDelegate.onMaskChanged(this, maskRect);
158168
if (onMaskChangedListener != null) {
159169
onMaskChangedListener.onMaskChanged(maskRect);

‎lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java

+108-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static com.google.android.material.carousel.CarouselHelper.assertChildrenHaveValidOrder;
1919
import static com.google.android.material.carousel.CarouselHelper.createDataSetWithSize;
20+
import static com.google.android.material.carousel.CarouselHelper.getKeylineMaskPercentage;
2021
import static com.google.android.material.carousel.CarouselHelper.getTestCenteredKeylineState;
2122
import static com.google.android.material.carousel.CarouselHelper.scrollHorizontallyBy;
2223
import static com.google.android.material.carousel.CarouselHelper.scrollToPosition;
@@ -25,6 +26,8 @@
2526
import static com.google.common.truth.Truth.assertThat;
2627

2728
import android.content.Context;
29+
import android.graphics.Rect;
30+
import android.graphics.RectF;
2831
import androidx.recyclerview.widget.RecyclerView;
2932
import android.view.View;
3033
import androidx.annotation.NonNull;
@@ -192,8 +195,7 @@ public void testScrollAndFill_shouldRecycleAndFillMinimumItemCountForContainer()
192195
@Test
193196
public void testEmptyAdapter_shouldClearAllViewsFromRecyclerView() throws Throwable {
194197
// Fill the adapter and then empty it to make sure all views are removed and recycled
195-
setAdapterItems(
196-
recyclerView, layoutManager, adapter, CarouselHelper.createDataSetWithSize(200));
198+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(200));
197199
scrollToPosition(recyclerView, layoutManager, 100);
198200
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(0));
199201

@@ -202,31 +204,30 @@ public void testEmptyAdapter_shouldClearAllViewsFromRecyclerView() throws Throwa
202204

203205
@Test
204206
public void testSingleItem_shouldBeInFocalRange() throws Throwable {
205-
setAdapterItems(recyclerView, layoutManager, adapter, CarouselHelper.createDataSetWithSize(1));
207+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(1));
206208

207209
assertThat(((Maskable) recyclerView.getChildAt(0)).getMaskXPercentage()).isEqualTo(0F);
208210
}
209211

210212
@Test
211213
public void testSingleItem_shouldNotScrollLeft() throws Throwable {
212-
setAdapterItems(recyclerView, layoutManager, adapter, CarouselHelper.createDataSetWithSize(1));
214+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(1));
213215
scrollHorizontallyBy(recyclerView, layoutManager, 100);
214216

215217
assertThat(recyclerView.getChildAt(0).getLeft()).isEqualTo(0);
216218
}
217219

218220
@Test
219221
public void testSingleItem_shouldNotScrollRight() throws Throwable {
220-
setAdapterItems(recyclerView, layoutManager, adapter, CarouselHelper.createDataSetWithSize(1));
222+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(1));
221223
scrollHorizontallyBy(recyclerView, layoutManager, -100);
222224

223225
assertThat(recyclerView.getChildAt(0).getLeft()).isEqualTo(0);
224226
}
225227

226228
@Test
227229
public void testChangeAdapterItemCount_shouldAlignFirstItemToStart() throws Throwable {
228-
setAdapterItems(
229-
recyclerView, layoutManager, adapter, CarouselHelper.createDataSetWithSize(200));
230+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(200));
230231
scrollToPosition(recyclerView, layoutManager, 100);
231232
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(1));
232233

@@ -236,30 +237,78 @@ public void testChangeAdapterItemCount_shouldAlignFirstItemToStart() throws Thro
236237

237238
@Test
238239
public void testScrollToEnd_childrenHaveValidOrder() throws Throwable {
239-
setAdapterItems(recyclerView, layoutManager, adapter, CarouselHelper.createDataSetWithSize(10));
240+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
240241
scrollToPosition(recyclerView, layoutManager, 9);
241242

242243
assertChildrenHaveValidOrder(layoutManager);
243244
}
244245

245246
@Test
246247
public void testScrollToMiddle_childrenHaveValidOrder() throws Throwable {
247-
setAdapterItems(
248-
recyclerView, layoutManager, adapter, CarouselHelper.createDataSetWithSize(200));
248+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(200));
249249
scrollToPosition(recyclerView, layoutManager, 99);
250250

251251
assertChildrenHaveValidOrder(layoutManager);
252252
}
253253

254254
@Test
255255
public void testScrollToEndThenToStart_childrenHaveValidOrder() throws Throwable {
256-
setAdapterItems(recyclerView, layoutManager, adapter, CarouselHelper.createDataSetWithSize(10));
256+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
257257
scrollToPosition(recyclerView, layoutManager, 9);
258258
scrollToPosition(recyclerView, layoutManager, 2);
259259

260260
assertChildrenHaveValidOrder(layoutManager);
261261
}
262262

263+
@Test
264+
public void testContainedLayout_doesNotAllowFirstItemToBleed() throws Throwable {
265+
layoutManager.setCarouselStrategy(new TestContainmentCarouselStrategy(/* isContained= */ true));
266+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
267+
scrollHorizontallyBy(recyclerView, layoutManager, 900);
268+
269+
Rect firstChildMask =
270+
getMaskRectOffsetToRecyclerViewCoords((MaskableFrameLayout) recyclerView.getChildAt(0));
271+
assertThat(firstChildMask.left).isAtLeast(0);
272+
}
273+
274+
@Test
275+
public void testContainedLayout_doesNotAllowLastItemToBleed() throws Throwable {
276+
layoutManager.setCarouselStrategy(new TestContainmentCarouselStrategy(/* isContained= */ true));
277+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
278+
scrollToPosition(recyclerView, layoutManager, 5);
279+
scrollHorizontallyBy(recyclerView, layoutManager, -165);
280+
281+
Rect lastChildMask =
282+
getMaskRectOffsetToRecyclerViewCoords(
283+
(MaskableFrameLayout) recyclerView.getChildAt(recyclerView.getChildCount() - 1));
284+
assertThat(lastChildMask.right).isAtMost(DEFAULT_RECYCLER_VIEW_WIDTH);
285+
}
286+
287+
@Test
288+
public void testUncontainedLayout_allowsFistItemToBleed() throws Throwable {
289+
layoutManager.setCarouselStrategy(
290+
new TestContainmentCarouselStrategy(/* isContained= */ false));
291+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
292+
scrollHorizontallyBy(recyclerView, layoutManager, 900);
293+
294+
Rect firstItemMask =
295+
getMaskRectOffsetToRecyclerViewCoords((MaskableFrameLayout) recyclerView.getChildAt(0));
296+
assertThat(firstItemMask.left).isLessThan(0);
297+
}
298+
299+
@Test
300+
public void testUncontainedLayout_allowsLastItemToBleed() throws Throwable {
301+
layoutManager.setCarouselStrategy(
302+
new TestContainmentCarouselStrategy(/* isContained= */ false));
303+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
304+
scrollHorizontallyBy(recyclerView, layoutManager, 900);
305+
306+
Rect lastItemMask =
307+
getMaskRectOffsetToRecyclerViewCoords(
308+
(MaskableFrameLayout) recyclerView.getChildAt(recyclerView.getChildCount() - 1));
309+
assertThat(lastItemMask.right).isGreaterThan(DEFAULT_RECYCLER_VIEW_WIDTH);
310+
}
311+
263312
/**
264313
* Assigns explicit sizes to fixtures being used to construct the testing environment.
265314
*
@@ -277,4 +326,52 @@ private void createAndSetFixtures(int recyclerWidth, int itemWidth) {
277326
recyclerView.setLayoutManager(layoutManager);
278327
recyclerView.setAdapter(adapter);
279328
}
329+
330+
/**
331+
* Gets the bounds of {@code child}'s mask after they are offset to the parent RecyclerView's
332+
* coordinates
333+
*/
334+
private Rect getMaskRectOffsetToRecyclerViewCoords(MaskableFrameLayout child) {
335+
RectF maskRect = child.getMaskRectF();
336+
Rect offsetRect =
337+
new Rect(
338+
(int) maskRect.left, (int) maskRect.top, (int) maskRect.right, (int) maskRect.bottom);
339+
recyclerView.offsetDescendantRectToMyCoords(child, offsetRect);
340+
return offsetRect;
341+
}
342+
343+
/**
344+
* A CarouselStrategy used to test that items are masked correctly when contained vs. uncontained.
345+
*/
346+
private static class TestContainmentCarouselStrategy extends CarouselStrategy {
347+
348+
private final boolean isContained;
349+
350+
TestContainmentCarouselStrategy(boolean isContained) {
351+
this.isContained = isContained;
352+
}
353+
354+
@Override
355+
KeylineState onFirstChildMeasuredWithMargins(@NonNull Carousel carousel, @NonNull View child) {
356+
float largeSize = DEFAULT_RECYCLER_VIEW_WIDTH * .75F; // 990F
357+
float smallSize = DEFAULT_RECYCLER_VIEW_WIDTH - largeSize; // 330F
358+
float xSmallSize = 100F;
359+
360+
float xSmallHead = xSmallSize / -2F;
361+
float focal = largeSize / 2F;
362+
float smallTail = focal + (largeSize / 2F) + (smallSize / 2F);
363+
float xSmallTail = DEFAULT_RECYCLER_VIEW_WIDTH + (xSmallSize / 2F);
364+
return new KeylineState.Builder(largeSize)
365+
.addKeyline(xSmallHead, getKeylineMaskPercentage(xSmallSize, largeSize), xSmallSize)
366+
.addKeyline(focal, 0F, largeSize, true)
367+
.addKeyline(smallTail, getKeylineMaskPercentage(smallSize, largeSize), smallSize)
368+
.addKeyline(xSmallTail, getKeylineMaskPercentage(xSmallSize, largeSize), xSmallSize)
369+
.build();
370+
}
371+
372+
@Override
373+
boolean isContained() {
374+
return isContained;
375+
}
376+
}
280377
}

‎lib/javatests/com/google/android/material/carousel/MaskableFrameLayoutTest.java

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public void testShapeAppearanceWithAbsoluteCornerSizes_shouldBeClamped() {
6767
ShapeAppearanceModel model =
6868
new ShapeAppearanceModel.Builder().setAllCornerSizes(new AbsoluteCornerSize(200F)).build();
6969
maskableFrameLayout.setShapeAppearanceModel(model);
70+
maskableFrameLayout.setMaskRectF(new RectF(0F, 0F, 50F, 50F));
7071
CornerSize topRightCornerSize =
7172
maskableFrameLayout.getShapeAppearanceModel().getTopRightCornerSize();
7273

@@ -80,6 +81,7 @@ public void testShapeAppearanceWithAbsoluteCornerSizes_shouldBeClamped() {
8081
public void testForceCompatClipping_shouldNotUseViewOutlineProvider() {
8182
MaskableFrameLayout maskableFrameLayout = createMaskableFrameLayoutWithSize(50, 50);
8283
ShapeAppearanceModel model = new ShapeAppearanceModel.Builder().setAllCornerSizes(10F).build();
84+
maskableFrameLayout.setMaskRectF(new RectF(0F, 0F, 50F, 50F));
8385
maskableFrameLayout.setShapeAppearanceModel(model);
8486

8587
assertThat(maskableFrameLayout.getClipToOutline()).isTrue();
@@ -94,6 +96,7 @@ public void testRoundedCornersApi22_usesViewOutlineProvider() {
9496
MaskableFrameLayout maskableFrameLayout = createMaskableFrameLayoutWithSize(50, 50);
9597
ShapeAppearanceModel model = new ShapeAppearanceModel.Builder().setAllCornerSizes(10F).build();
9698
maskableFrameLayout.setShapeAppearanceModel(model);
99+
maskableFrameLayout.setMaskRectF(new RectF(0F, 0F, 50F, 50F));
97100

98101
assertThat(maskableFrameLayout.getClipToOutline()).isTrue();
99102
}

0 commit comments

Comments
 (0)
Please sign in to comment.