Skip to content

Commit

Permalink
[Shape] Add interpolation between default and an arbitrary corner rad…
Browse files Browse the repository at this point in the history
…ius for Android Material Views.

PiperOrigin-RevId: 626446451
  • Loading branch information
kendrickumstattd authored and leticiarossi committed Apr 23, 2024
1 parent 9b09b69 commit cc125d9
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 15 deletions.
64 changes: 63 additions & 1 deletion lib/java/com/google/android/material/shape/CornerTreatment.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class CornerTreatment {
public void getCornerPath(float angle, float interpolation, @NonNull ShapePath shapePath) {}

/**
* Generates a {@link ShapePath} for this corner treatment.
* Generates a {@link ShapePath} using a single radius value for this corner treatment.
*
* <p>CornerTreatments are assumed to have an origin of (0, 0) (i.e. they represent the top-left
* corner), and are automatically rotated and scaled as necessary when applied to other corners.
Expand All @@ -70,6 +70,32 @@ public void getCornerPath(
getCornerPath(angle, interpolation, shapePath);
}

/**
* Generates a {@link ShapePath} using start and end radius values for this corner treatment.
*
* <p>CornerTreatments are assumed to have an origin of (0, 0) (i.e. they represent the top-left
* corner), and are automatically rotated and scaled as necessary when applied to other corners.
*
* @param shapePath the {@link ShapePath} that this treatment should write to.
* @param angle the angle of the corner, typically 90 degrees.
* @param interpolation the interpolation of the corner treatment. Ranges between 0 (none) and 1
* (fully) interpolated. Custom corner treatments can implement interpolation to support shape
* transition between two arbitrary states. Typically, a value of 0 indicates that the custom
* corner treatment is not rendered (i.e. that it is a 90 degree angle), and a value of 1
* indicates that the treatment is fully rendered. Animation between these two values can
* "heal" or "reveal" a corner treatment.
* @param startRadius the starting radius or size of this corner before interpolation.
* @param endRadius the ending radius or size of this corner after interpolation.
*/
public void getCornerPath(
@NonNull ShapePath shapePath,
float angle,
float interpolation,
float startRadius,
float endRadius) {
getCornerPath(shapePath, angle, interpolation, endRadius);
}

/**
* Generates a {@link ShapePath} for this corner treatment.
*
Expand Down Expand Up @@ -97,4 +123,40 @@ public void getCornerPath(
@NonNull CornerSize size) {
getCornerPath(shapePath, angle, interpolation, size.getCornerSize(bounds));
}

/**
* Generates a {@link ShapePath} using start and end {@link CornerSize} values for this corner
* treatment.
*
* <p>CornerTreatments are assumed to have an origin of (0, 0) (i.e. they represent the top-left
* corner), and are automatically rotated and scaled as necessary when applied to other corners.
*
* @param shapePath the {@link ShapePath} that this treatment should write to.
* @param angle the angle of the corner, typically 90 degrees.
* @param interpolation the interpolation of the corner treatment. Ranges between 0 (none) and 1
* (fully) interpolated. Custom corner treatments can implement interpolation to support shape
* transition between two arbitrary states. Typically, a value of 0 indicates that the custom
* corner treatment is not rendered (i.e. that it is a 90 degree angle), and a value of 1
* indicates that the treatment is fully rendered. Animation between these two values can
* "heal" or "reveal" a corner treatment.
* @param bounds the bounds of the full shape that will be drawn. This could be used change the
* behavior of the CornerTreatment depending on how much space is available for the full
* shape.
* @param startSize the starting {@link CornerSize} used for this corner before interpolation
* @param endSize the ending {@link CornerSize} used for this corner after interpolation
*/
public void getCornerPath(
@NonNull ShapePath shapePath,
float angle,
float interpolation,
@NonNull RectF bounds,
@NonNull CornerSize startSize,
@NonNull CornerSize endSize) {
getCornerPath(
shapePath,
angle,
interpolation,
startSize.getCornerSize(bounds),
endSize.getCornerSize(bounds));
}
}
19 changes: 16 additions & 3 deletions lib/java/com/google/android/material/shape/CutCornerTreatment.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.google.android.material.shape;

import static com.google.android.material.math.MathUtils.lerp;

import androidx.annotation.NonNull;

/** A corner treatment which cuts or clips the original corner of a shape with a straight line. */
Expand Down Expand Up @@ -44,11 +46,22 @@ public CutCornerTreatment(float size) {
@Override
public void getCornerPath(
@NonNull ShapePath shapePath, float angle, float interpolation, float radius) {
shapePath.reset(0, radius * interpolation, ShapePath.ANGLE_LEFT, 180 - angle);
getCornerPath(shapePath, angle, interpolation, 0, radius);
}

@Override
public void getCornerPath(
@NonNull ShapePath shapePath,
float angle,
float interpolation,
float startRadius,
float endRadius) {
float radius = lerp(startRadius, endRadius, interpolation);
shapePath.reset(0, radius, ShapePath.ANGLE_LEFT, 180 - angle);
shapePath.lineTo(
(float) (Math.sin(Math.toRadians(angle)) * radius * interpolation),
(float) (Math.sin(Math.toRadians(angle)) * radius),
// Something about using cos() is causing rounding which prevents the path from being convex
// on api levels 21 and 22. Using sin() with 90 - angle is helping for now.
(float) (Math.sin(Math.toRadians(90 - angle)) * radius * interpolation));
(float) (Math.sin(Math.toRadians(90 - angle)) * radius));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.android.material.R;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static com.google.android.material.math.MathUtils.lerp;

import android.annotation.TargetApi;
import android.content.Context;
Expand Down Expand Up @@ -81,6 +82,9 @@ public class MaterialShapeDrawable extends Drawable implements TintAwareDrawable

private static final float SHADOW_OFFSET_MULTIPLIER = .25f;

static final ShapeAppearanceModel DEFAULT_INTERPOLATION_START_SHAPE_APPEARANCE_MODEL =
ShapeAppearanceModel.builder().setAllCorners(CornerFamily.ROUNDED, 0).build();

/**
* Try to draw native elevation shadows if possible, otherwise use fake shadows. This is best for
* paths which will always be convex. If the path might change to be concave, you should consider
Expand Down Expand Up @@ -293,6 +297,35 @@ public ShapeAppearanceModel getShapeAppearanceModel() {
return drawableState.shapeAppearanceModel;
}

/**
* Set the shape appearance when interpolation is 0.
*
* @param startShape the ShapeAppearanceModel for the shape when interpolation is 0. The edge
* treatments within it are ignored.
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void setInterpolationStartShapeAppearanceModel(@NonNull ShapeAppearanceModel startShape) {
if (drawableState.interpolationStartShapeAppearanceModel != startShape) {
drawableState.interpolationStartShapeAppearanceModel = startShape;
pathDirty = true;
invalidateSelf();
}
}

/**
* Get the {@link ShapeAppearanceModel} containing the path that should be rendered at the
* beginning of interpolation (when interpolation=0).
*
* @return the starting model.
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@NonNull
public ShapeAppearanceModel getInterpolationStartShapeAppearanceModel() {
return drawableState.interpolationStartShapeAppearanceModel;
}

/**
* Set the {@link ShapePathModel} containing the path that will be rendered in this drawable.
*
Expand Down Expand Up @@ -1071,10 +1104,11 @@ private void drawShape(
@NonNull ShapeAppearanceModel shapeAppearanceModel,
@NonNull RectF bounds) {
if (shapeAppearanceModel.isRoundRect(bounds)) {
float cornerSize =
shapeAppearanceModel.getTopRightCornerSize().getCornerSize(bounds)
* drawableState.interpolation;
canvas.drawRoundRect(bounds, cornerSize, cornerSize, paint);
float endRadius = shapeAppearanceModel.getTopLeftCornerSize().getCornerSize(bounds);
shapeAppearanceModel = drawableState.interpolationStartShapeAppearanceModel;
float startRadius = shapeAppearanceModel.getTopLeftCornerSize().getCornerSize(bounds);
float radius = lerp(startRadius, endRadius, drawableState.interpolation);
canvas.drawRoundRect(bounds, radius, radius, paint);
} else {
canvas.drawPath(path, paint);
}
Expand Down Expand Up @@ -1183,6 +1217,7 @@ public void getPathForSize(int width, int height, @NonNull Path path) {
protected final void calculatePathForSize(@NonNull RectF bounds, @NonNull Path path) {
pathProvider.calculatePath(
drawableState.shapeAppearanceModel,
drawableState.interpolationStartShapeAppearanceModel,
drawableState.interpolation,
bounds,
pathShadowListener,
Expand Down Expand Up @@ -1211,8 +1246,10 @@ public CornerSize apply(@NonNull CornerSize cornerSize) {

pathProvider.calculatePath(
strokeShapeAppearance,
drawableState.interpolationStartShapeAppearanceModel,
drawableState.interpolation,
getBoundsInsetByStroke(),
null,
pathInsetByStroke);
}

Expand All @@ -1225,11 +1262,16 @@ public void getOutline(@NonNull Outline outline) {
}

if (isRoundRect()) {
float radius = getTopLeftCornerResolvedSize() * drawableState.interpolation;
float startRadius =
drawableState
.interpolationStartShapeAppearanceModel
.getTopLeftCornerSize()
.getCornerSize(getBoundsAsRectF());
float endRadius = getTopLeftCornerResolvedSize();
float radius = lerp(startRadius, endRadius, drawableState.interpolation);
outline.setRoundRect(getBounds(), radius);
return;
}

calculatePath(getBoundsAsRectF(), path);
DrawableUtils.setOutlineToPath(outline, path);
}
Expand Down Expand Up @@ -1409,7 +1451,8 @@ public float getBottomRightCornerResolvedSize() {
*/
@RestrictTo(LIBRARY_GROUP)
public boolean isRoundRect() {
return drawableState.shapeAppearanceModel.isRoundRect(getBoundsAsRectF());
return drawableState.shapeAppearanceModel.isRoundRect(getBoundsAsRectF())
&& drawableState.interpolationStartShapeAppearanceModel.isRoundRect(getBoundsAsRectF());
}

/**
Expand All @@ -1421,6 +1464,8 @@ public boolean isRoundRect() {
protected static class MaterialShapeDrawableState extends ConstantState {

@NonNull ShapeAppearanceModel shapeAppearanceModel;
// The shape appearance when interpolation is 0. Edge treatments are ignored.
@NonNull ShapeAppearanceModel interpolationStartShapeAppearanceModel;
@Nullable ElevationOverlayProvider elevationOverlayProvider;

@Nullable ColorFilter colorFilter;
Expand Down Expand Up @@ -1453,10 +1498,13 @@ public MaterialShapeDrawableState(
@Nullable ElevationOverlayProvider elevationOverlayProvider) {
this.shapeAppearanceModel = shapeAppearanceModel;
this.elevationOverlayProvider = elevationOverlayProvider;
this.interpolationStartShapeAppearanceModel =
DEFAULT_INTERPOLATION_START_SHAPE_APPEARANCE_MODEL;
}

public MaterialShapeDrawableState(@NonNull MaterialShapeDrawableState orig) {
shapeAppearanceModel = orig.shapeAppearanceModel;
interpolationStartShapeAppearanceModel = orig.interpolationStartShapeAppearanceModel;
elevationOverlayProvider = orig.elevationOverlayProvider;
strokeWidth = orig.strokeWidth;
colorFilter = orig.colorFilter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.google.android.material.shape;

import static com.google.android.material.math.MathUtils.lerp;

import androidx.annotation.NonNull;

/** A corner treatment which rounds a corner of a shape. */
Expand All @@ -40,7 +42,18 @@ public RoundedCornerTreatment(float radius) {
@Override
public void getCornerPath(
@NonNull ShapePath shapePath, float angle, float interpolation, float radius) {
shapePath.reset(0, radius * interpolation, ShapePath.ANGLE_LEFT, 180 - angle);
shapePath.addArc(0, 0, 2 * radius * interpolation, 2 * radius * interpolation, 180, angle);
getCornerPath(shapePath, angle, interpolation, 0, radius);
}

@Override
public void getCornerPath(
@NonNull ShapePath shapePath,
float angle,
float interpolation,
float startRadius,
float endRadius) {
float radius = lerp(startRadius, endRadius, interpolation);
shapePath.reset(0, radius, ShapePath.ANGLE_LEFT, 180 - angle);
shapePath.addArc(0, 0, 2 * radius, 2 * radius, 180, angle);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ private static class Lazy {

/**
* Listener called every time a {@link ShapePath} is created for a corner or an edge treatment.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public interface PathListener {
Expand Down Expand Up @@ -76,6 +78,7 @@ public ShapeAppearancePathProvider() {
}
}

/** @hide */
@UiThread
@RestrictTo(LIBRARY_GROUP)
@NonNull
Expand Down Expand Up @@ -107,10 +110,42 @@ public void calculatePath(
* @param bounds the desired bounds for the path.
* @param pathListener the path
* @param path the returned path out-var.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void calculatePath(
ShapeAppearanceModel shapeAppearanceModel,
float interpolation,
RectF bounds,
PathListener pathListener,
@NonNull Path path) {
calculatePath(
shapeAppearanceModel,
MaterialShapeDrawable.DEFAULT_INTERPOLATION_START_SHAPE_APPEARANCE_MODEL,
interpolation,
bounds,
pathListener,
path);
}

/**
* Writes the given {@link ShapeAppearanceModel} to {@code path}
*
* @param shapeAppearanceModel The shape to be applied in the path.
* @param interpolationStartShapeAppearanceModel The shape to be applied in the path when
* interpolation is 0.
* @param interpolation the desired interpolation.
* @param bounds the desired bounds for the path.
* @param pathListener the path
* @param path the returned path out-var.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void calculatePath(
ShapeAppearanceModel shapeAppearanceModel,
@NonNull ShapeAppearanceModel interpolationStartShapeAppearanceModel,
float interpolation,
RectF bounds,
PathListener pathListener,
Expand All @@ -121,7 +156,12 @@ public void calculatePath(
boundsPath.addRect(bounds, Direction.CW);
ShapeAppearancePathSpec spec =
new ShapeAppearancePathSpec(
shapeAppearanceModel, interpolation, bounds, pathListener, path);
shapeAppearanceModel,
interpolationStartShapeAppearanceModel,
interpolation,
bounds,
pathListener,
path);

// Calculate the transformations (rotations and translations) necessary for each edge and
// corner treatment.
Expand All @@ -146,8 +186,10 @@ public void calculatePath(

private void setCornerPathAndTransform(@NonNull ShapeAppearancePathSpec spec, int index) {
CornerSize size = getCornerSizeForIndex(index, spec.shapeAppearanceModel);
CornerSize startSize =
getCornerSizeForIndex(index, spec.interpolationStartShapeAppearanceModel);
getCornerTreatmentForIndex(index, spec.shapeAppearanceModel)
.getCornerPath(cornerPaths[index], 90, spec.interpolation, spec.bounds, size);
.getCornerPath(cornerPaths[index], 90, spec.interpolation, spec.bounds, startSize, size);

float edgeAngle = angleOfEdge(index);
cornerTransforms[index].reset();
Expand Down Expand Up @@ -333,6 +375,7 @@ void setEdgeIntersectionCheckEnable(boolean enable) {
static final class ShapeAppearancePathSpec {

@NonNull public final ShapeAppearanceModel shapeAppearanceModel;
@NonNull public final ShapeAppearanceModel interpolationStartShapeAppearanceModel;
@NonNull public final Path path;
@NonNull public final RectF bounds;

Expand All @@ -342,12 +385,14 @@ static final class ShapeAppearancePathSpec {

ShapeAppearancePathSpec(
@NonNull ShapeAppearanceModel shapeAppearanceModel,
@NonNull ShapeAppearanceModel interpolationStartShapeAppearanceModel,
float interpolation,
RectF bounds,
@Nullable PathListener pathListener,
Path path) {
this.pathListener = pathListener;
this.shapeAppearanceModel = shapeAppearanceModel;
this.interpolationStartShapeAppearanceModel = interpolationStartShapeAppearanceModel;
this.interpolation = interpolation;
this.bounds = bounds;
this.path = path;
Expand Down
Loading

0 comments on commit cc125d9

Please sign in to comment.