Skip to content

Commit 14023d2

Browse files
hunterstichpaulfthomas
authored andcommitted
[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 #3450 PiperOrigin-RevId: 546859519
1 parent 839b14c commit 14023d2

File tree

4 files changed

+86
-18
lines changed

4 files changed

+86
-18
lines changed

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

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@
4040
/** A {@link FrameLayout} than is able to mask itself and all children. */
4141
public class MaskableFrameLayout extends FrameLayout implements Maskable, Shapeable {
4242

43-
private float maskXPercentage = 0F;
44-
private final RectF maskRect = new RectF();
43+
private static final float MASK_X_PERCENTAGE_UNSET = -1F;
44+
45+
private float maskXPercentage = MASK_X_PERCENTAGE_UNSET;
46+
@Nullable private RectF maskRect = null;
4547
@Nullable private OnMaskChangedListener onMaskChangedListener;
4648
@NonNull private ShapeAppearanceModel shapeAppearanceModel;
4749
private final ShapeableDelegate shapeableDelegate = ShapeableDelegate.create(this);
@@ -65,7 +67,13 @@ public MaskableFrameLayout(
6567
@Override
6668
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
6769
super.onSizeChanged(w, h, oldw, oldh);
68-
onMaskChanged();
70+
if (maskXPercentage != MASK_X_PERCENTAGE_UNSET) {
71+
// If the mask x percentage has been set, the mask rect needs to be recalculated by calling
72+
// setMaskXPercentage which will then handle calling onMaskChanged
73+
setMaskXPercentage(maskXPercentage);
74+
} else {
75+
onMaskChanged();
76+
}
6977
}
7078

7179
@Override
@@ -120,23 +128,28 @@ public ShapeAppearanceModel getShapeAppearanceModel() {
120128
@Override
121129
@Deprecated
122130
public void setMaskXPercentage(float percentage) {
123-
percentage = MathUtils.clamp(percentage, 0F, 1F);
124-
if (maskXPercentage != percentage) {
125-
this.maskXPercentage = percentage;
126-
// Translate the percentage into an actual pixel value of how much of this view should be
127-
// masked away.
128-
float maskWidth = AnimationUtils.lerp(0f, getWidth() / 2F, 0f, 1f, maskXPercentage);
129-
setMaskRectF(new RectF(maskWidth, 0F, (getWidth() - maskWidth), getHeight()));
130-
}
131+
this.maskXPercentage = MathUtils.clamp(percentage, 0F, 1F);
132+
// Translate the percentage into an actual pixel value of how much of this view should be
133+
// masked away.
134+
float maskWidth = AnimationUtils.lerp(0f, getWidth() / 2F, 0f, 1f, maskXPercentage);
135+
updateMaskRectF(new RectF(maskWidth, 0F, (getWidth() - maskWidth), getHeight()));
131136
}
132137

133138
/**
134139
* Sets the {@link RectF} that this {@link View} will be masked by.
135140
*
141+
* <p>Calling this method will overwrite any mask set using {@link #setMaskXPercentage(float)}.
142+
*
136143
* @param maskRect a rect in the view's coordinates to mask by
137144
*/
138145
@Override
139146
public void setMaskRectF(@NonNull RectF maskRect) {
147+
this.maskXPercentage = MASK_X_PERCENTAGE_UNSET;
148+
updateMaskRectF(maskRect);
149+
}
150+
151+
private void updateMaskRectF(@NonNull RectF maskRect) {
152+
ensureMaskRectF();
140153
this.maskRect.set(maskRect);
141154
onMaskChanged();
142155
}
@@ -158,21 +171,28 @@ public float getMaskXPercentage() {
158171
@NonNull
159172
@Override
160173
public RectF getMaskRectF() {
174+
ensureMaskRectF();
161175
return maskRect;
162176
}
163177

178+
private void ensureMaskRectF() {
179+
if (maskRect == null) {
180+
maskRect = new RectF(0F, 0F, getWidth(), getHeight());
181+
}
182+
}
183+
164184
@Override
165185
public void setOnMaskChangedListener(@Nullable OnMaskChangedListener onMaskChangedListener) {
166186
this.onMaskChangedListener = onMaskChangedListener;
167187
}
168188

169189
private void onMaskChanged() {
170-
if (getWidth() == 0) {
190+
if (getWidth() == 0 || getHeight() == 0) {
171191
return;
172192
}
173-
shapeableDelegate.onMaskChanged(this, maskRect);
193+
shapeableDelegate.onMaskChanged(this, getMaskRectF());
174194
if (onMaskChangedListener != null) {
175-
onMaskChangedListener.onMaskChanged(maskRect);
195+
onMaskChangedListener.onMaskChanged(getMaskRectF());
176196
}
177197
}
178198

@@ -191,10 +211,10 @@ public void setForceCompatClipping(boolean forceCompatClipping) {
191211
@Override
192212
public boolean onTouchEvent(MotionEvent event) {
193213
// Only handle touch events that are within the masked bounds of this view.
194-
if (!maskRect.isEmpty() && event.getAction() == MotionEvent.ACTION_DOWN) {
214+
if (!getMaskRectF().isEmpty() && event.getAction() == MotionEvent.ACTION_DOWN) {
195215
float x = event.getX();
196216
float y = event.getY();
197-
if (!maskRect.contains(x, y)) {
217+
if (!getMaskRectF().contains(x, y)) {
198218
return false;
199219
}
200220
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ KeylineState onFirstChildMeasuredWithMargins(
114114
public void testSingleItem_shouldBeInFocalRange() throws Throwable {
115115
setAdapterItems(recyclerView, layoutManager, adapter, CarouselHelper.createDataSetWithSize(1));
116116

117-
assertThat(((Maskable) recyclerView.getChildAt(0)).getMaskXPercentage()).isEqualTo(0F);
117+
assertThat(recyclerView.getChildAt(0).getWidth()).isEqualTo(DEFAULT_ITEM_WIDTH);
118118
}
119119

120120
@Test

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ public void testEmptyAdapter_shouldClearAllViewsFromRecyclerView() throws Throwa
255255
public void testSingleItem_shouldBeInFocalRange() throws Throwable {
256256
setAdapterItems(recyclerView, layoutManager, adapter, createDataSetWithSize(1));
257257

258-
assertThat(((Maskable) recyclerView.getChildAt(0)).getMaskXPercentage()).isEqualTo(0F);
258+
assertThat(recyclerView.getChildAt(0).getWidth()).isEqualTo(DEFAULT_ITEM_WIDTH);
259259
}
260260

261261
@Test

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,54 @@ public void testCutCornersApi33_usesViewOutlineProvider() {
131131
assertThat(maskableFrameLayout.getClipToOutline()).isTrue();
132132
}
133133

134+
@Test
135+
public void testUseMaskRect_shouldIgnoreMaskXPercentage() {
136+
MaskableFrameLayout maskableFrameLayout = createMaskableFrameLayoutWithSize(50, 50);
137+
ShapeAppearanceModel model = new ShapeAppearanceModel.Builder().setAllCornerSizes(10F).build();
138+
maskableFrameLayout.setShapeAppearanceModel(model);
139+
140+
maskableFrameLayout.setMaskXPercentage(.5F);
141+
maskableFrameLayout.setMaskRectF(new RectF(0F, 0F, 50F, 50F));
142+
143+
assertThat(maskableFrameLayout.getMaskXPercentage()).isEqualTo(-1F);
144+
}
145+
146+
@Test
147+
public void testOnSizeChangedWithMaskXPercentageSet_shouldUpdateMaskRect() {
148+
MaskableFrameLayout maskableFrameLayout = createMaskableFrameLayoutWithSize(50, 50);
149+
ShapeAppearanceModel model = new ShapeAppearanceModel.Builder().setAllCornerSizes(10F).build();
150+
maskableFrameLayout.setShapeAppearanceModel(model);
151+
maskableFrameLayout.setMaskXPercentage(.5F);
152+
153+
maskableFrameLayout.setLayoutParams(new LayoutParams(100, 100));
154+
maskableFrameLayout.measure(
155+
MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY),
156+
MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY));
157+
maskableFrameLayout.layout(
158+
0, 0, maskableFrameLayout.getMeasuredWidth(), maskableFrameLayout.getMeasuredHeight());
159+
160+
assertThat(maskableFrameLayout.getMaskRectF()).isEqualTo(new RectF(25F, 0F, 75F, 100F));
161+
}
162+
163+
@Test
164+
public void testOnSizeChangedWithMaskRect_shouldNotChangeMaskRect() {
165+
MaskableFrameLayout maskableFrameLayout = createMaskableFrameLayoutWithSize(50, 50);
166+
ShapeAppearanceModel model = new ShapeAppearanceModel.Builder().setAllCornerSizes(10F).build();
167+
maskableFrameLayout.setShapeAppearanceModel(model);
168+
169+
RectF mask = new RectF(10F, 0F, 40F, 50F);
170+
maskableFrameLayout.setMaskRectF(mask);
171+
172+
maskableFrameLayout.setLayoutParams(new LayoutParams(100, 100));
173+
maskableFrameLayout.measure(
174+
MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY),
175+
MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY));
176+
maskableFrameLayout.layout(
177+
0, 0, maskableFrameLayout.getMeasuredWidth(), maskableFrameLayout.getMeasuredHeight());
178+
179+
assertThat(maskableFrameLayout.getMaskRectF()).isEqualTo(mask);
180+
}
181+
134182
private static MaskableFrameLayout createMaskableFrameLayoutWithSize(int width, int height) {
135183
MaskableFrameLayout maskableFrameLayout =
136184
new MaskableFrameLayout(ApplicationProvider.getApplicationContext());

0 commit comments

Comments
 (0)