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());