From 9486de5f2fba5c15cb85ac7ac27f30a724162f12 Mon Sep 17 00:00:00 2001 From: rightnao Date: Tue, 13 Jun 2023 23:18:50 +0000 Subject: [PATCH] [Carousel] Ensure that masks are pushed out beyond the parent bounds if they are _on_ the parent bounds PiperOrigin-RevId: 540105089 --- .../carousel/CarouselLayoutManager.java | 28 ++++++--- .../carousel/CarouselLayoutManagerTest.java | 62 +++++++++++++++++++ 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/lib/java/com/google/android/material/carousel/CarouselLayoutManager.java b/lib/java/com/google/android/material/carousel/CarouselLayoutManager.java index cca90162965..0d1c42f9968 100644 --- a/lib/java/com/google/android/material/carousel/CarouselLayoutManager.java +++ b/lib/java/com/google/android/material/carousel/CarouselLayoutManager.java @@ -784,21 +784,35 @@ private void updateChildMaskForLocation( float maskWidth = lerp(0F, childWidth / 2F, 0F, 1F, maskProgress); RectF maskRect = new RectF(maskWidth, 0F, (childWidth - maskWidth), childHeight); + float offsetCx = calculateChildOffsetCenterForLocation(child, childCenterLocation, range); + float maskedLeft = offsetCx - (maskRect.width() / 2F); + float maskedRight = offsetCx + (maskRect.width() / 2F); + // If the carousel is a CONTAINED carousel, ensure the mask collapses against the side of the // 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()) { - float offsetCx = calculateChildOffsetCenterForLocation(child, childCenterLocation, range); - float maskedLeft = offsetCx - (maskRect.width() / 2F); - float maskedRight = offsetCx + (maskRect.width() / 2F); - if (maskedLeft < getParentLeft() && maskedRight >= getParentLeft()) { - maskRect.left = maskRect.left + (getParentLeft() - maskedLeft); + if (maskedLeft < getParentLeft() && maskedRight > getParentLeft()) { + float diff = getParentLeft() - maskedLeft; + maskRect.left += diff; + maskedLeft += diff; } - if (maskedRight > getParentRight() && maskedLeft <= getParentRight()) { - maskRect.right = max(maskRect.right - (maskedRight - getParentRight()), maskRect.left); + if (maskedRight > getParentRight() && maskedLeft < getParentRight()) { + float diff = maskedRight - getParentRight(); + maskRect.right = max(maskRect.right - diff, maskRect.left); + maskedRight = max(maskedRight - diff, maskedLeft); } } + + // 'Push out' any masks that are on the parent edge by rounding up/down and adding or + // subtracting a pixel. Otherwise, the mask on the 'edge' looks like it has a width of 1 pixel. + if (maskedRight <= getParentLeft()) { + maskRect.right = (float) Math.floor(maskRect.right) - 1; + } + if (maskedLeft >= getParentRight()) { + maskRect.left = (float) Math.ceil(maskRect.left) + 1; + } ((Maskable) child).setMaskRectF(maskRect); } diff --git a/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java b/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java index e84f285c058..1600a2307f4 100644 --- a/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java +++ b/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java @@ -309,6 +309,68 @@ public void testUncontainedLayout_allowsLastItemToBleed() throws Throwable { assertThat(lastItemMask.right).isGreaterThan(DEFAULT_RECYCLER_VIEW_WIDTH); } + @Test + public void testMasksLeftOfParent_areRoundedDown() throws Throwable { + layoutManager.setCarouselStrategy( + new TestContainmentCarouselStrategy(/* isContained= */ false)); + setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); + scrollHorizontallyBy(recyclerView, layoutManager, 900); + + for (int i = 0; i < recyclerView.getChildCount(); i++) { + View child = recyclerView.getChildAt(i); + Rect itemMask = getMaskRectOffsetToRecyclerViewCoords((MaskableFrameLayout) child); + assertThat(itemMask.right).isNotEqualTo(0); + } + } + + @Test + public void testMaskOnLeftParentEdge_areRoundedUp() throws Throwable { + layoutManager.setCarouselStrategy( + new TestContainmentCarouselStrategy(/* isContained= */ false)); + setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); + // Scroll to end + scrollToPosition(recyclerView, layoutManager, 9); + + // Carousel strategy at end is {small, large}. Last child will be large item, second last + // child will be small item. So third last child's right mask edge should not show. + Rect thirdLastChildMask = + getMaskRectOffsetToRecyclerViewCoords( + (MaskableFrameLayout) recyclerView.getChildAt(recyclerView.getChildCount() - 3)); + assertThat(thirdLastChildMask.right).isLessThan(0); + assertThat(thirdLastChildMask.right).isAtLeast(thirdLastChildMask.left); + } + + @Test + public void testMaskOnRightBoundary_areRoundedUp() throws Throwable { + layoutManager.setCarouselStrategy( + new TestContainmentCarouselStrategy(/* isContained= */ false)); + setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); + scrollHorizontallyBy(recyclerView, layoutManager, 900); + + // For every child, assert that the mask left edge is located beyond the recycler view + // width (parent's right edge). + for (int i = recyclerView.getChildCount() - 1; i >= 0; i--) { + View child = recyclerView.getChildAt(i); + Rect itemMask = getMaskRectOffsetToRecyclerViewCoords((MaskableFrameLayout) child); + assertThat(itemMask.left).isNotEqualTo(DEFAULT_RECYCLER_VIEW_WIDTH); + } + } + + @Test + public void testMaskOnRightParentEdge_areRoundedUp() throws Throwable { + layoutManager.setCarouselStrategy( + new TestContainmentCarouselStrategy(/* isContained= */ false)); + setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(10)); + + // Carousel strategy is {large, small}. First child will be large item, second child will + // be small item, so the third child's left mask edge should not show up at the right parent + // edge. + Rect thirdChildMask = + getMaskRectOffsetToRecyclerViewCoords((MaskableFrameLayout) recyclerView.getChildAt(2)); + assertThat(thirdChildMask.left).isGreaterThan(DEFAULT_RECYCLER_VIEW_WIDTH); + assertThat(thirdChildMask.left).isAtMost(thirdChildMask.right); + } + /** * Assigns explicit sizes to fixtures being used to construct the testing environment. *