Skip to content

Commit 9486de5

Browse files
imhappipaulfthomas
authored andcommitted
[Carousel] Ensure that masks are pushed out beyond the parent bounds if they are _on_ the parent bounds
PiperOrigin-RevId: 540105089
1 parent 022e217 commit 9486de5

File tree

2 files changed

+83
-7
lines changed

2 files changed

+83
-7
lines changed

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -784,21 +784,35 @@ private void updateChildMaskForLocation(
784784
float maskWidth = lerp(0F, childWidth / 2F, 0F, 1F, maskProgress);
785785
RectF maskRect = new RectF(maskWidth, 0F, (childWidth - maskWidth), childHeight);
786786

787+
float offsetCx = calculateChildOffsetCenterForLocation(child, childCenterLocation, range);
788+
float maskedLeft = offsetCx - (maskRect.width() / 2F);
789+
float maskedRight = offsetCx + (maskRect.width() / 2F);
790+
787791
// If the carousel is a CONTAINED carousel, ensure the mask collapses against the side of the
788792
// container instead of bleeding and being clipped by the RecyclerView's bounds.
789793
// Only do this if there is only one side of the mask that is out of bounds; if
790794
// both sides are out of bounds on the same side, then the whole mask is out of view.
791795
if (carouselStrategy.isContained()) {
792-
float offsetCx = calculateChildOffsetCenterForLocation(child, childCenterLocation, range);
793-
float maskedLeft = offsetCx - (maskRect.width() / 2F);
794-
float maskedRight = offsetCx + (maskRect.width() / 2F);
795-
if (maskedLeft < getParentLeft() && maskedRight >= getParentLeft()) {
796-
maskRect.left = maskRect.left + (getParentLeft() - maskedLeft);
796+
if (maskedLeft < getParentLeft() && maskedRight > getParentLeft()) {
797+
float diff = getParentLeft() - maskedLeft;
798+
maskRect.left += diff;
799+
maskedLeft += diff;
797800
}
798-
if (maskedRight > getParentRight() && maskedLeft <= getParentRight()) {
799-
maskRect.right = max(maskRect.right - (maskedRight - getParentRight()), maskRect.left);
801+
if (maskedRight > getParentRight() && maskedLeft < getParentRight()) {
802+
float diff = maskedRight - getParentRight();
803+
maskRect.right = max(maskRect.right - diff, maskRect.left);
804+
maskedRight = max(maskedRight - diff, maskedLeft);
800805
}
801806
}
807+
808+
// 'Push out' any masks that are on the parent edge by rounding up/down and adding or
809+
// subtracting a pixel. Otherwise, the mask on the 'edge' looks like it has a width of 1 pixel.
810+
if (maskedRight <= getParentLeft()) {
811+
maskRect.right = (float) Math.floor(maskRect.right) - 1;
812+
}
813+
if (maskedLeft >= getParentRight()) {
814+
maskRect.left = (float) Math.ceil(maskRect.left) + 1;
815+
}
802816
((Maskable) child).setMaskRectF(maskRect);
803817
}
804818

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,68 @@ public void testUncontainedLayout_allowsLastItemToBleed() throws Throwable {
309309
assertThat(lastItemMask.right).isGreaterThan(DEFAULT_RECYCLER_VIEW_WIDTH);
310310
}
311311

312+
@Test
313+
public void testMasksLeftOfParent_areRoundedDown() throws Throwable {
314+
layoutManager.setCarouselStrategy(
315+
new TestContainmentCarouselStrategy(/* isContained= */ false));
316+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
317+
scrollHorizontallyBy(recyclerView, layoutManager, 900);
318+
319+
for (int i = 0; i < recyclerView.getChildCount(); i++) {
320+
View child = recyclerView.getChildAt(i);
321+
Rect itemMask = getMaskRectOffsetToRecyclerViewCoords((MaskableFrameLayout) child);
322+
assertThat(itemMask.right).isNotEqualTo(0);
323+
}
324+
}
325+
326+
@Test
327+
public void testMaskOnLeftParentEdge_areRoundedUp() throws Throwable {
328+
layoutManager.setCarouselStrategy(
329+
new TestContainmentCarouselStrategy(/* isContained= */ false));
330+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
331+
// Scroll to end
332+
scrollToPosition(recyclerView, layoutManager, 9);
333+
334+
// Carousel strategy at end is {small, large}. Last child will be large item, second last
335+
// child will be small item. So third last child's right mask edge should not show.
336+
Rect thirdLastChildMask =
337+
getMaskRectOffsetToRecyclerViewCoords(
338+
(MaskableFrameLayout) recyclerView.getChildAt(recyclerView.getChildCount() - 3));
339+
assertThat(thirdLastChildMask.right).isLessThan(0);
340+
assertThat(thirdLastChildMask.right).isAtLeast(thirdLastChildMask.left);
341+
}
342+
343+
@Test
344+
public void testMaskOnRightBoundary_areRoundedUp() throws Throwable {
345+
layoutManager.setCarouselStrategy(
346+
new TestContainmentCarouselStrategy(/* isContained= */ false));
347+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
348+
scrollHorizontallyBy(recyclerView, layoutManager, 900);
349+
350+
// For every child, assert that the mask left edge is located beyond the recycler view
351+
// width (parent's right edge).
352+
for (int i = recyclerView.getChildCount() - 1; i >= 0; i--) {
353+
View child = recyclerView.getChildAt(i);
354+
Rect itemMask = getMaskRectOffsetToRecyclerViewCoords((MaskableFrameLayout) child);
355+
assertThat(itemMask.left).isNotEqualTo(DEFAULT_RECYCLER_VIEW_WIDTH);
356+
}
357+
}
358+
359+
@Test
360+
public void testMaskOnRightParentEdge_areRoundedUp() throws Throwable {
361+
layoutManager.setCarouselStrategy(
362+
new TestContainmentCarouselStrategy(/* isContained= */ false));
363+
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10));
364+
365+
// Carousel strategy is {large, small}. First child will be large item, second child will
366+
// be small item, so the third child's left mask edge should not show up at the right parent
367+
// edge.
368+
Rect thirdChildMask =
369+
getMaskRectOffsetToRecyclerViewCoords((MaskableFrameLayout) recyclerView.getChildAt(2));
370+
assertThat(thirdChildMask.left).isGreaterThan(DEFAULT_RECYCLER_VIEW_WIDTH);
371+
assertThat(thirdChildMask.left).isAtMost(thirdChildMask.right);
372+
}
373+
312374
/**
313375
* Assigns explicit sizes to fixtures being used to construct the testing environment.
314376
*

0 commit comments

Comments
 (0)