From 14023d2c8589a515ae0d80f75f301077aa324f08 Mon Sep 17 00:00:00 2001 From: hunterstich Date: Mon, 10 Jul 2023 13:57:25 +0000 Subject: [PATCH] [Carousel] Fixed MaskableFrameLayout not updating mask after size change when setting the mask using setMaskXPercentage. This also fixes the default list catalog demo not displaying any items due to every item's mask having an empty maskRect. Resolves https://github.com/material-components/material-components-android/pull/3450 PiperOrigin-RevId: 546859519 --- .../carousel/MaskableFrameLayout.java | 52 +++++++++++++------ .../CarouselLayoutManagerRtlTest.java | 2 +- .../carousel/CarouselLayoutManagerTest.java | 2 +- .../carousel/MaskableFrameLayoutTest.java | 48 +++++++++++++++++ 4 files changed, 86 insertions(+), 18 deletions(-) diff --git a/lib/java/com/google/android/material/carousel/MaskableFrameLayout.java b/lib/java/com/google/android/material/carousel/MaskableFrameLayout.java index 264838c2b97..d071a4c5f7f 100644 --- a/lib/java/com/google/android/material/carousel/MaskableFrameLayout.java +++ b/lib/java/com/google/android/material/carousel/MaskableFrameLayout.java @@ -40,8 +40,10 @@ /** A {@link FrameLayout} than is able to mask itself and all children. */ public class MaskableFrameLayout extends FrameLayout implements Maskable, Shapeable { - private float maskXPercentage = 0F; - private final RectF maskRect = new RectF(); + private static final float MASK_X_PERCENTAGE_UNSET = -1F; + + private float maskXPercentage = MASK_X_PERCENTAGE_UNSET; + @Nullable private RectF maskRect = null; @Nullable private OnMaskChangedListener onMaskChangedListener; @NonNull private ShapeAppearanceModel shapeAppearanceModel; private final ShapeableDelegate shapeableDelegate = ShapeableDelegate.create(this); @@ -65,7 +67,13 @@ public MaskableFrameLayout( @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); - onMaskChanged(); + if (maskXPercentage != MASK_X_PERCENTAGE_UNSET) { + // If the mask x percentage has been set, the mask rect needs to be recalculated by calling + // setMaskXPercentage which will then handle calling onMaskChanged + setMaskXPercentage(maskXPercentage); + } else { + onMaskChanged(); + } } @Override @@ -120,23 +128,28 @@ public ShapeAppearanceModel getShapeAppearanceModel() { @Override @Deprecated public void setMaskXPercentage(float percentage) { - percentage = MathUtils.clamp(percentage, 0F, 1F); - if (maskXPercentage != percentage) { - this.maskXPercentage = percentage; - // Translate the percentage into an actual pixel value of how much of this view should be - // masked away. - float maskWidth = AnimationUtils.lerp(0f, getWidth() / 2F, 0f, 1f, maskXPercentage); - setMaskRectF(new RectF(maskWidth, 0F, (getWidth() - maskWidth), getHeight())); - } + this.maskXPercentage = MathUtils.clamp(percentage, 0F, 1F); + // Translate the percentage into an actual pixel value of how much of this view should be + // masked away. + float maskWidth = AnimationUtils.lerp(0f, getWidth() / 2F, 0f, 1f, maskXPercentage); + updateMaskRectF(new RectF(maskWidth, 0F, (getWidth() - maskWidth), getHeight())); } /** * Sets the {@link RectF} that this {@link View} will be masked by. * + *

Calling this method will overwrite any mask set using {@link #setMaskXPercentage(float)}. + * * @param maskRect a rect in the view's coordinates to mask by */ @Override public void setMaskRectF(@NonNull RectF maskRect) { + this.maskXPercentage = MASK_X_PERCENTAGE_UNSET; + updateMaskRectF(maskRect); + } + + private void updateMaskRectF(@NonNull RectF maskRect) { + ensureMaskRectF(); this.maskRect.set(maskRect); onMaskChanged(); } @@ -158,21 +171,28 @@ public float getMaskXPercentage() { @NonNull @Override public RectF getMaskRectF() { + ensureMaskRectF(); return maskRect; } + private void ensureMaskRectF() { + if (maskRect == null) { + maskRect = new RectF(0F, 0F, getWidth(), getHeight()); + } + } + @Override public void setOnMaskChangedListener(@Nullable OnMaskChangedListener onMaskChangedListener) { this.onMaskChangedListener = onMaskChangedListener; } private void onMaskChanged() { - if (getWidth() == 0) { + if (getWidth() == 0 || getHeight() == 0) { return; } - shapeableDelegate.onMaskChanged(this, maskRect); + shapeableDelegate.onMaskChanged(this, getMaskRectF()); if (onMaskChangedListener != null) { - onMaskChangedListener.onMaskChanged(maskRect); + onMaskChangedListener.onMaskChanged(getMaskRectF()); } } @@ -191,10 +211,10 @@ public void setForceCompatClipping(boolean forceCompatClipping) { @Override public boolean onTouchEvent(MotionEvent event) { // Only handle touch events that are within the masked bounds of this view. - if (!maskRect.isEmpty() && event.getAction() == MotionEvent.ACTION_DOWN) { + if (!getMaskRectF().isEmpty() && event.getAction() == MotionEvent.ACTION_DOWN) { float x = event.getX(); float y = event.getY(); - if (!maskRect.contains(x, y)) { + if (!getMaskRectF().contains(x, y)) { return false; } } diff --git a/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerRtlTest.java b/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerRtlTest.java index 8022308322d..e2366f4a175 100644 --- a/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerRtlTest.java +++ b/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerRtlTest.java @@ -114,7 +114,7 @@ KeylineState onFirstChildMeasuredWithMargins( public void testSingleItem_shouldBeInFocalRange() throws Throwable { setAdapterItems(recyclerView, layoutManager, adapter, CarouselHelper.createDataSetWithSize(1)); - assertThat(((Maskable) recyclerView.getChildAt(0)).getMaskXPercentage()).isEqualTo(0F); + assertThat(recyclerView.getChildAt(0).getWidth()).isEqualTo(DEFAULT_ITEM_WIDTH); } @Test diff --git a/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java b/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java index 035108b2c15..9fc2f43b89a 100644 --- a/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java +++ b/lib/javatests/com/google/android/material/carousel/CarouselLayoutManagerTest.java @@ -255,7 +255,7 @@ public void testEmptyAdapter_shouldClearAllViewsFromRecyclerView() throws Throwa public void testSingleItem_shouldBeInFocalRange() throws Throwable { setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(1)); - assertThat(((Maskable) recyclerView.getChildAt(0)).getMaskXPercentage()).isEqualTo(0F); + assertThat(recyclerView.getChildAt(0).getWidth()).isEqualTo(DEFAULT_ITEM_WIDTH); } @Test diff --git a/lib/javatests/com/google/android/material/carousel/MaskableFrameLayoutTest.java b/lib/javatests/com/google/android/material/carousel/MaskableFrameLayoutTest.java index 0ab8f17ce30..f5c505bbdc7 100644 --- a/lib/javatests/com/google/android/material/carousel/MaskableFrameLayoutTest.java +++ b/lib/javatests/com/google/android/material/carousel/MaskableFrameLayoutTest.java @@ -131,6 +131,54 @@ public void testCutCornersApi33_usesViewOutlineProvider() { assertThat(maskableFrameLayout.getClipToOutline()).isTrue(); } + @Test + public void testUseMaskRect_shouldIgnoreMaskXPercentage() { + MaskableFrameLayout maskableFrameLayout = createMaskableFrameLayoutWithSize(50, 50); + ShapeAppearanceModel model = new ShapeAppearanceModel.Builder().setAllCornerSizes(10F).build(); + maskableFrameLayout.setShapeAppearanceModel(model); + + maskableFrameLayout.setMaskXPercentage(.5F); + maskableFrameLayout.setMaskRectF(new RectF(0F, 0F, 50F, 50F)); + + assertThat(maskableFrameLayout.getMaskXPercentage()).isEqualTo(-1F); + } + + @Test + public void testOnSizeChangedWithMaskXPercentageSet_shouldUpdateMaskRect() { + MaskableFrameLayout maskableFrameLayout = createMaskableFrameLayoutWithSize(50, 50); + ShapeAppearanceModel model = new ShapeAppearanceModel.Builder().setAllCornerSizes(10F).build(); + maskableFrameLayout.setShapeAppearanceModel(model); + maskableFrameLayout.setMaskXPercentage(.5F); + + maskableFrameLayout.setLayoutParams(new LayoutParams(100, 100)); + maskableFrameLayout.measure( + MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY)); + maskableFrameLayout.layout( + 0, 0, maskableFrameLayout.getMeasuredWidth(), maskableFrameLayout.getMeasuredHeight()); + + assertThat(maskableFrameLayout.getMaskRectF()).isEqualTo(new RectF(25F, 0F, 75F, 100F)); + } + + @Test + public void testOnSizeChangedWithMaskRect_shouldNotChangeMaskRect() { + MaskableFrameLayout maskableFrameLayout = createMaskableFrameLayoutWithSize(50, 50); + ShapeAppearanceModel model = new ShapeAppearanceModel.Builder().setAllCornerSizes(10F).build(); + maskableFrameLayout.setShapeAppearanceModel(model); + + RectF mask = new RectF(10F, 0F, 40F, 50F); + maskableFrameLayout.setMaskRectF(mask); + + maskableFrameLayout.setLayoutParams(new LayoutParams(100, 100)); + maskableFrameLayout.measure( + MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY)); + maskableFrameLayout.layout( + 0, 0, maskableFrameLayout.getMeasuredWidth(), maskableFrameLayout.getMeasuredHeight()); + + assertThat(maskableFrameLayout.getMaskRectF()).isEqualTo(mask); + } + private static MaskableFrameLayout createMaskableFrameLayoutWithSize(int width, int height) { MaskableFrameLayout maskableFrameLayout = new MaskableFrameLayout(ApplicationProvider.getApplicationContext());