Skip to content

Commit cc125d9

Browse files
kendrickumstattdleticiarossi
authored andcommitted
[Shape] Add interpolation between default and an arbitrary corner radius for Android Material Views.
PiperOrigin-RevId: 626446451
1 parent 9b09b69 commit cc125d9

File tree

6 files changed

+208
-15
lines changed

6 files changed

+208
-15
lines changed

lib/java/com/google/android/material/shape/CornerTreatment.java

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class CornerTreatment {
5050
public void getCornerPath(float angle, float interpolation, @NonNull ShapePath shapePath) {}
5151

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

73+
/**
74+
* Generates a {@link ShapePath} using start and end radius values for this corner treatment.
75+
*
76+
* <p>CornerTreatments are assumed to have an origin of (0, 0) (i.e. they represent the top-left
77+
* corner), and are automatically rotated and scaled as necessary when applied to other corners.
78+
*
79+
* @param shapePath the {@link ShapePath} that this treatment should write to.
80+
* @param angle the angle of the corner, typically 90 degrees.
81+
* @param interpolation the interpolation of the corner treatment. Ranges between 0 (none) and 1
82+
* (fully) interpolated. Custom corner treatments can implement interpolation to support shape
83+
* transition between two arbitrary states. Typically, a value of 0 indicates that the custom
84+
* corner treatment is not rendered (i.e. that it is a 90 degree angle), and a value of 1
85+
* indicates that the treatment is fully rendered. Animation between these two values can
86+
* "heal" or "reveal" a corner treatment.
87+
* @param startRadius the starting radius or size of this corner before interpolation.
88+
* @param endRadius the ending radius or size of this corner after interpolation.
89+
*/
90+
public void getCornerPath(
91+
@NonNull ShapePath shapePath,
92+
float angle,
93+
float interpolation,
94+
float startRadius,
95+
float endRadius) {
96+
getCornerPath(shapePath, angle, interpolation, endRadius);
97+
}
98+
7399
/**
74100
* Generates a {@link ShapePath} for this corner treatment.
75101
*
@@ -97,4 +123,40 @@ public void getCornerPath(
97123
@NonNull CornerSize size) {
98124
getCornerPath(shapePath, angle, interpolation, size.getCornerSize(bounds));
99125
}
126+
127+
/**
128+
* Generates a {@link ShapePath} using start and end {@link CornerSize} values for this corner
129+
* treatment.
130+
*
131+
* <p>CornerTreatments are assumed to have an origin of (0, 0) (i.e. they represent the top-left
132+
* corner), and are automatically rotated and scaled as necessary when applied to other corners.
133+
*
134+
* @param shapePath the {@link ShapePath} that this treatment should write to.
135+
* @param angle the angle of the corner, typically 90 degrees.
136+
* @param interpolation the interpolation of the corner treatment. Ranges between 0 (none) and 1
137+
* (fully) interpolated. Custom corner treatments can implement interpolation to support shape
138+
* transition between two arbitrary states. Typically, a value of 0 indicates that the custom
139+
* corner treatment is not rendered (i.e. that it is a 90 degree angle), and a value of 1
140+
* indicates that the treatment is fully rendered. Animation between these two values can
141+
* "heal" or "reveal" a corner treatment.
142+
* @param bounds the bounds of the full shape that will be drawn. This could be used change the
143+
* behavior of the CornerTreatment depending on how much space is available for the full
144+
* shape.
145+
* @param startSize the starting {@link CornerSize} used for this corner before interpolation
146+
* @param endSize the ending {@link CornerSize} used for this corner after interpolation
147+
*/
148+
public void getCornerPath(
149+
@NonNull ShapePath shapePath,
150+
float angle,
151+
float interpolation,
152+
@NonNull RectF bounds,
153+
@NonNull CornerSize startSize,
154+
@NonNull CornerSize endSize) {
155+
getCornerPath(
156+
shapePath,
157+
angle,
158+
interpolation,
159+
startSize.getCornerSize(bounds),
160+
endSize.getCornerSize(bounds));
161+
}
100162
}

lib/java/com/google/android/material/shape/CutCornerTreatment.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package com.google.android.material.shape;
1818

19+
import static com.google.android.material.math.MathUtils.lerp;
20+
1921
import androidx.annotation.NonNull;
2022

2123
/** A corner treatment which cuts or clips the original corner of a shape with a straight line. */
@@ -44,11 +46,22 @@ public CutCornerTreatment(float size) {
4446
@Override
4547
public void getCornerPath(
4648
@NonNull ShapePath shapePath, float angle, float interpolation, float radius) {
47-
shapePath.reset(0, radius * interpolation, ShapePath.ANGLE_LEFT, 180 - angle);
49+
getCornerPath(shapePath, angle, interpolation, 0, radius);
50+
}
51+
52+
@Override
53+
public void getCornerPath(
54+
@NonNull ShapePath shapePath,
55+
float angle,
56+
float interpolation,
57+
float startRadius,
58+
float endRadius) {
59+
float radius = lerp(startRadius, endRadius, interpolation);
60+
shapePath.reset(0, radius, ShapePath.ANGLE_LEFT, 180 - angle);
4861
shapePath.lineTo(
49-
(float) (Math.sin(Math.toRadians(angle)) * radius * interpolation),
62+
(float) (Math.sin(Math.toRadians(angle)) * radius),
5063
// Something about using cos() is causing rounding which prevents the path from being convex
5164
// on api levels 21 and 22. Using sin() with 90 - angle is helping for now.
52-
(float) (Math.sin(Math.toRadians(90 - angle)) * radius * interpolation));
65+
(float) (Math.sin(Math.toRadians(90 - angle)) * radius));
5366
}
5467
}

lib/java/com/google/android/material/shape/MaterialShapeDrawable.java

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.android.material.R;
2020

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

2324
import android.annotation.TargetApi;
2425
import android.content.Context;
@@ -81,6 +82,9 @@ public class MaterialShapeDrawable extends Drawable implements TintAwareDrawable
8182

8283
private static final float SHADOW_OFFSET_MULTIPLIER = .25f;
8384

85+
static final ShapeAppearanceModel DEFAULT_INTERPOLATION_START_SHAPE_APPEARANCE_MODEL =
86+
ShapeAppearanceModel.builder().setAllCorners(CornerFamily.ROUNDED, 0).build();
87+
8488
/**
8589
* Try to draw native elevation shadows if possible, otherwise use fake shadows. This is best for
8690
* paths which will always be convex. If the path might change to be concave, you should consider
@@ -293,6 +297,35 @@ public ShapeAppearanceModel getShapeAppearanceModel() {
293297
return drawableState.shapeAppearanceModel;
294298
}
295299

300+
/**
301+
* Set the shape appearance when interpolation is 0.
302+
*
303+
* @param startShape the ShapeAppearanceModel for the shape when interpolation is 0. The edge
304+
* treatments within it are ignored.
305+
* @hide
306+
*/
307+
@RestrictTo(LIBRARY_GROUP)
308+
public void setInterpolationStartShapeAppearanceModel(@NonNull ShapeAppearanceModel startShape) {
309+
if (drawableState.interpolationStartShapeAppearanceModel != startShape) {
310+
drawableState.interpolationStartShapeAppearanceModel = startShape;
311+
pathDirty = true;
312+
invalidateSelf();
313+
}
314+
}
315+
316+
/**
317+
* Get the {@link ShapeAppearanceModel} containing the path that should be rendered at the
318+
* beginning of interpolation (when interpolation=0).
319+
*
320+
* @return the starting model.
321+
* @hide
322+
*/
323+
@RestrictTo(LIBRARY_GROUP)
324+
@NonNull
325+
public ShapeAppearanceModel getInterpolationStartShapeAppearanceModel() {
326+
return drawableState.interpolationStartShapeAppearanceModel;
327+
}
328+
296329
/**
297330
* Set the {@link ShapePathModel} containing the path that will be rendered in this drawable.
298331
*
@@ -1071,10 +1104,11 @@ private void drawShape(
10711104
@NonNull ShapeAppearanceModel shapeAppearanceModel,
10721105
@NonNull RectF bounds) {
10731106
if (shapeAppearanceModel.isRoundRect(bounds)) {
1074-
float cornerSize =
1075-
shapeAppearanceModel.getTopRightCornerSize().getCornerSize(bounds)
1076-
* drawableState.interpolation;
1077-
canvas.drawRoundRect(bounds, cornerSize, cornerSize, paint);
1107+
float endRadius = shapeAppearanceModel.getTopLeftCornerSize().getCornerSize(bounds);
1108+
shapeAppearanceModel = drawableState.interpolationStartShapeAppearanceModel;
1109+
float startRadius = shapeAppearanceModel.getTopLeftCornerSize().getCornerSize(bounds);
1110+
float radius = lerp(startRadius, endRadius, drawableState.interpolation);
1111+
canvas.drawRoundRect(bounds, radius, radius, paint);
10781112
} else {
10791113
canvas.drawPath(path, paint);
10801114
}
@@ -1183,6 +1217,7 @@ public void getPathForSize(int width, int height, @NonNull Path path) {
11831217
protected final void calculatePathForSize(@NonNull RectF bounds, @NonNull Path path) {
11841218
pathProvider.calculatePath(
11851219
drawableState.shapeAppearanceModel,
1220+
drawableState.interpolationStartShapeAppearanceModel,
11861221
drawableState.interpolation,
11871222
bounds,
11881223
pathShadowListener,
@@ -1211,8 +1246,10 @@ public CornerSize apply(@NonNull CornerSize cornerSize) {
12111246

12121247
pathProvider.calculatePath(
12131248
strokeShapeAppearance,
1249+
drawableState.interpolationStartShapeAppearanceModel,
12141250
drawableState.interpolation,
12151251
getBoundsInsetByStroke(),
1252+
null,
12161253
pathInsetByStroke);
12171254
}
12181255

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

12271264
if (isRoundRect()) {
1228-
float radius = getTopLeftCornerResolvedSize() * drawableState.interpolation;
1265+
float startRadius =
1266+
drawableState
1267+
.interpolationStartShapeAppearanceModel
1268+
.getTopLeftCornerSize()
1269+
.getCornerSize(getBoundsAsRectF());
1270+
float endRadius = getTopLeftCornerResolvedSize();
1271+
float radius = lerp(startRadius, endRadius, drawableState.interpolation);
12291272
outline.setRoundRect(getBounds(), radius);
12301273
return;
12311274
}
1232-
12331275
calculatePath(getBoundsAsRectF(), path);
12341276
DrawableUtils.setOutlineToPath(outline, path);
12351277
}
@@ -1409,7 +1451,8 @@ public float getBottomRightCornerResolvedSize() {
14091451
*/
14101452
@RestrictTo(LIBRARY_GROUP)
14111453
public boolean isRoundRect() {
1412-
return drawableState.shapeAppearanceModel.isRoundRect(getBoundsAsRectF());
1454+
return drawableState.shapeAppearanceModel.isRoundRect(getBoundsAsRectF())
1455+
&& drawableState.interpolationStartShapeAppearanceModel.isRoundRect(getBoundsAsRectF());
14131456
}
14141457

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

14231466
@NonNull ShapeAppearanceModel shapeAppearanceModel;
1467+
// The shape appearance when interpolation is 0. Edge treatments are ignored.
1468+
@NonNull ShapeAppearanceModel interpolationStartShapeAppearanceModel;
14241469
@Nullable ElevationOverlayProvider elevationOverlayProvider;
14251470

14261471
@Nullable ColorFilter colorFilter;
@@ -1453,10 +1498,13 @@ public MaterialShapeDrawableState(
14531498
@Nullable ElevationOverlayProvider elevationOverlayProvider) {
14541499
this.shapeAppearanceModel = shapeAppearanceModel;
14551500
this.elevationOverlayProvider = elevationOverlayProvider;
1501+
this.interpolationStartShapeAppearanceModel =
1502+
DEFAULT_INTERPOLATION_START_SHAPE_APPEARANCE_MODEL;
14561503
}
14571504

14581505
public MaterialShapeDrawableState(@NonNull MaterialShapeDrawableState orig) {
14591506
shapeAppearanceModel = orig.shapeAppearanceModel;
1507+
interpolationStartShapeAppearanceModel = orig.interpolationStartShapeAppearanceModel;
14601508
elevationOverlayProvider = orig.elevationOverlayProvider;
14611509
strokeWidth = orig.strokeWidth;
14621510
colorFilter = orig.colorFilter;

lib/java/com/google/android/material/shape/RoundedCornerTreatment.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package com.google.android.material.shape;
1818

19+
import static com.google.android.material.math.MathUtils.lerp;
20+
1921
import androidx.annotation.NonNull;
2022

2123
/** A corner treatment which rounds a corner of a shape. */
@@ -40,7 +42,18 @@ public RoundedCornerTreatment(float radius) {
4042
@Override
4143
public void getCornerPath(
4244
@NonNull ShapePath shapePath, float angle, float interpolation, float radius) {
43-
shapePath.reset(0, radius * interpolation, ShapePath.ANGLE_LEFT, 180 - angle);
44-
shapePath.addArc(0, 0, 2 * radius * interpolation, 2 * radius * interpolation, 180, angle);
45+
getCornerPath(shapePath, angle, interpolation, 0, radius);
46+
}
47+
48+
@Override
49+
public void getCornerPath(
50+
@NonNull ShapePath shapePath,
51+
float angle,
52+
float interpolation,
53+
float startRadius,
54+
float endRadius) {
55+
float radius = lerp(startRadius, endRadius, interpolation);
56+
shapePath.reset(0, radius, ShapePath.ANGLE_LEFT, 180 - angle);
57+
shapePath.addArc(0, 0, 2 * radius, 2 * radius, 180, angle);
4558
}
4659
}

lib/java/com/google/android/material/shape/ShapeAppearancePathProvider.java

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ private static class Lazy {
4141

4242
/**
4343
* Listener called every time a {@link ShapePath} is created for a corner or an edge treatment.
44+
*
45+
* @hide
4446
*/
4547
@RestrictTo(LIBRARY_GROUP)
4648
public interface PathListener {
@@ -76,6 +78,7 @@ public ShapeAppearancePathProvider() {
7678
}
7779
}
7880

81+
/** @hide */
7982
@UiThread
8083
@RestrictTo(LIBRARY_GROUP)
8184
@NonNull
@@ -107,10 +110,42 @@ public void calculatePath(
107110
* @param bounds the desired bounds for the path.
108111
* @param pathListener the path
109112
* @param path the returned path out-var.
113+
*
114+
* @hide
115+
*/
116+
@RestrictTo(LIBRARY_GROUP)
117+
public void calculatePath(
118+
ShapeAppearanceModel shapeAppearanceModel,
119+
float interpolation,
120+
RectF bounds,
121+
PathListener pathListener,
122+
@NonNull Path path) {
123+
calculatePath(
124+
shapeAppearanceModel,
125+
MaterialShapeDrawable.DEFAULT_INTERPOLATION_START_SHAPE_APPEARANCE_MODEL,
126+
interpolation,
127+
bounds,
128+
pathListener,
129+
path);
130+
}
131+
132+
/**
133+
* Writes the given {@link ShapeAppearanceModel} to {@code path}
134+
*
135+
* @param shapeAppearanceModel The shape to be applied in the path.
136+
* @param interpolationStartShapeAppearanceModel The shape to be applied in the path when
137+
* interpolation is 0.
138+
* @param interpolation the desired interpolation.
139+
* @param bounds the desired bounds for the path.
140+
* @param pathListener the path
141+
* @param path the returned path out-var.
142+
*
143+
* @hide
110144
*/
111145
@RestrictTo(LIBRARY_GROUP)
112146
public void calculatePath(
113147
ShapeAppearanceModel shapeAppearanceModel,
148+
@NonNull ShapeAppearanceModel interpolationStartShapeAppearanceModel,
114149
float interpolation,
115150
RectF bounds,
116151
PathListener pathListener,
@@ -121,7 +156,12 @@ public void calculatePath(
121156
boundsPath.addRect(bounds, Direction.CW);
122157
ShapeAppearancePathSpec spec =
123158
new ShapeAppearancePathSpec(
124-
shapeAppearanceModel, interpolation, bounds, pathListener, path);
159+
shapeAppearanceModel,
160+
interpolationStartShapeAppearanceModel,
161+
interpolation,
162+
bounds,
163+
pathListener,
164+
path);
125165

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

147187
private void setCornerPathAndTransform(@NonNull ShapeAppearancePathSpec spec, int index) {
148188
CornerSize size = getCornerSizeForIndex(index, spec.shapeAppearanceModel);
189+
CornerSize startSize =
190+
getCornerSizeForIndex(index, spec.interpolationStartShapeAppearanceModel);
149191
getCornerTreatmentForIndex(index, spec.shapeAppearanceModel)
150-
.getCornerPath(cornerPaths[index], 90, spec.interpolation, spec.bounds, size);
192+
.getCornerPath(cornerPaths[index], 90, spec.interpolation, spec.bounds, startSize, size);
151193

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

335377
@NonNull public final ShapeAppearanceModel shapeAppearanceModel;
378+
@NonNull public final ShapeAppearanceModel interpolationStartShapeAppearanceModel;
336379
@NonNull public final Path path;
337380
@NonNull public final RectF bounds;
338381

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

343386
ShapeAppearancePathSpec(
344387
@NonNull ShapeAppearanceModel shapeAppearanceModel,
388+
@NonNull ShapeAppearanceModel interpolationStartShapeAppearanceModel,
345389
float interpolation,
346390
RectF bounds,
347391
@Nullable PathListener pathListener,
348392
Path path) {
349393
this.pathListener = pathListener;
350394
this.shapeAppearanceModel = shapeAppearanceModel;
395+
this.interpolationStartShapeAppearanceModel = interpolationStartShapeAppearanceModel;
351396
this.interpolation = interpolation;
352397
this.bounds = bounds;
353398
this.path = path;

0 commit comments

Comments
 (0)