Skip to content

Commit

Permalink
[Carousel] Fixed MaskableFrameLayout not updating mask after size cha…
Browse files Browse the repository at this point in the history
…nge 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
  • Loading branch information
hunterstich authored and paulfthomas committed Jul 11, 2023
1 parent 839b14c commit 14023d2
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 18 deletions.
Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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.
*
* <p>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();
}
Expand All @@ -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());
}
}

Expand All @@ -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;
}
}
Expand Down
Expand Up @@ -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
Expand Down
Expand Up @@ -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
Expand Down
Expand Up @@ -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());
Expand Down

0 comments on commit 14023d2

Please sign in to comment.