Skip to content

Commit 38ef896

Browse files
afohrmangsajith
authored andcommitted
Adjust corners of MaterialShapeDrawable's stroke to draw the stroke perfectly over the fill.
To draw the stroke with the correct corner radius, MaterialShapeDrawable now calculates what the corner radius should be and adjusts the stroke accordingly. Before this change, the corner radius was calculated such that the stroke didn't fully reach the edges of the shape. This change tweaks the corner radius calculation so that the stroke's path matches the fill's path and is overlaid directly on top of the fill's shape. PiperOrigin-RevId: 219139586
1 parent 3bc7afd commit 38ef896

File tree

1 file changed

+88
-22
lines changed

1 file changed

+88
-22
lines changed

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

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public class MaterialShapeDrawable extends Drawable implements TintAwareDrawable
107107
private final Path pathInsetByStroke = new Path();
108108
private final PointF pointF = new PointF();
109109
private final RectF rectF = new RectF();
110+
private final RectF insetRectF = new RectF();
110111
private final ShapePath shapePath = new ShapePath();
111112
private final Region transparentRegion = new Region();
112113
private final Region scratchRegion = new Region();
@@ -115,6 +116,7 @@ public class MaterialShapeDrawable extends Drawable implements TintAwareDrawable
115116

116117
private ShapeAppearanceModel shapeAppearanceModel;
117118
private int shadowCompatMode = SHADOW_COMPAT_MODE_DEFAULT;
119+
private boolean paintShadowEnabled = false;
118120
private boolean useTintColorForShadow = false;
119121
private float interpolation = 1f;
120122
private int shadowCompatElevation = 0;
@@ -465,6 +467,15 @@ public void setShadowEnabled(boolean shadowEnabled) {
465467
shadowEnabled ? SHADOW_COMPAT_MODE_DEFAULT : SHADOW_COMPAT_MODE_NEVER);
466468
}
467469

470+
/** TODO: Remove the paint shadow */
471+
public void setPaintShadowEnabled(boolean paintShadowEnabled) {
472+
this.paintShadowEnabled = paintShadowEnabled;
473+
shadowCompatMode = SHADOW_COMPAT_MODE_NEVER;
474+
// Backwards compatible defaults.
475+
shadowCompatElevation = 5;
476+
shadowCompatRadius = 10;
477+
}
478+
468479
/**
469480
* Get the interpolation of the path, between 0 and 1. Ranges between 0 (none) and 1 (fully)
470481
* interpolated.
@@ -579,7 +590,7 @@ public void setShadowRadius(int shadowRadius) {
579590
* 21 or when the shape is concave.
580591
*/
581592
private boolean requiresCompatShadow() {
582-
return VERSION.SDK_INT < VERSION_CODES.LOLLIPOP || !pathInsetByStroke.isConvex();
593+
return VERSION.SDK_INT < VERSION_CODES.LOLLIPOP || !path.isConvex();
583594
}
584595

585596
/**
@@ -705,7 +716,11 @@ public void draw(Canvas canvas) {
705716
final int prevStrokeAlpha = strokePaint.getAlpha();
706717
strokePaint.setAlpha(modulateAlpha(prevStrokeAlpha, alpha));
707718

708-
calculatePath(getBoundsInsetByStroke(), pathInsetByStroke);
719+
if (shadowCompatElevation > 0 && paintShadowEnabled) {
720+
fillPaint.setShadowLayer(shadowCompatRadius, 0, shadowCompatElevation, Color.BLACK);
721+
}
722+
723+
calculatePath(getBoundsAsRectF(), path);
709724
if (hasCompatShadow()) {
710725
// Save the canvas before changing the clip bounds.
711726
canvas.save();
@@ -746,22 +761,72 @@ public void draw(Canvas canvas) {
746761
strokePaint.setAlpha(prevStrokeAlpha);
747762
}
748763

749-
private void drawStrokeShape(Canvas canvas) {
750-
drawShape(canvas, strokePaint);
764+
/** Draw the path or try to draw a round rect if possible. */
765+
private void drawShape(Canvas canvas, Paint paint, Path path, RectF bounds) {
766+
if (shapeAppearanceModel.isRoundRect()) {
767+
float cornerSize = shapeAppearanceModel.getTopRightCorner().getCornerSize();
768+
canvas.drawRoundRect(bounds, cornerSize, cornerSize, paint);
769+
} else {
770+
canvas.drawPath(path, paint);
771+
}
751772
}
752773

753774
private void drawFillShape(Canvas canvas) {
754-
drawShape(canvas, fillPaint);
775+
drawShape(canvas, fillPaint, path, getFillBounds());
755776
}
756777

757-
/** Draw the path or try to draw a round rect if possible. */
758-
private void drawShape(Canvas canvas, Paint paint) {
759-
if (shapeAppearanceModel.isRoundRect()) {
760-
float cornerSize = shapeAppearanceModel.getTopRightCorner().getCornerSize();
761-
canvas.drawRoundRect(getBoundsInsetByStroke(), cornerSize, cornerSize, paint);
762-
} else {
763-
canvas.drawPath(pathInsetByStroke, paint);
778+
private RectF getFillBounds() {
779+
RectF fillBounds = getBoundsAsRectF();
780+
// If there's a stroke, inset the bounds by a hairline to prevent the fill from peeking out from
781+
// under the stroke.
782+
if (hasStroke()) {
783+
float hairline = 1f;
784+
fillBounds.inset(hairline, hairline);
764785
}
786+
return fillBounds;
787+
}
788+
789+
private void drawStrokeShape(Canvas canvas) {
790+
drawStrokeAdjustedForCornerSize(canvas);
791+
}
792+
793+
private void drawStrokeAdjustedForCornerSize(Canvas canvas) {
794+
float cornerSizeTopLeft = getShapeAppearanceModel().getTopLeftCorner().cornerSize;
795+
float cornerSizeTopRight = getShapeAppearanceModel().getTopRightCorner().cornerSize;
796+
float cornerSizeBottomRight = getShapeAppearanceModel().getBottomRightCorner().cornerSize;
797+
float cornerSizeBottomLeft = getShapeAppearanceModel().getBottomLeftCorner().cornerSize;
798+
799+
// Adjust corner radius in order to draw the stroke so that the corners of the background are
800+
// drawn on top of the edges.
801+
setShapeAppearanceCornerSize(
802+
adjustCornerSizeForStrokeSize(cornerSizeTopLeft),
803+
adjustCornerSizeForStrokeSize(cornerSizeTopRight),
804+
adjustCornerSizeForStrokeSize(cornerSizeBottomRight),
805+
adjustCornerSizeForStrokeSize(cornerSizeBottomLeft));
806+
807+
RectF boundsInsetByStroke = getBoundsInsetByStroke();
808+
calculatePath(boundsInsetByStroke, pathInsetByStroke);
809+
drawShape(canvas, strokePaint, pathInsetByStroke, boundsInsetByStroke);
810+
811+
// Set the corner radius back to its original size so that it draws on top of the fill.
812+
setShapeAppearanceCornerSize(
813+
cornerSizeTopLeft, cornerSizeTopRight, cornerSizeBottomRight, cornerSizeBottomLeft);
814+
}
815+
816+
private void setShapeAppearanceCornerSize(
817+
float cornerSizeTopLeft,
818+
float cornerSizeTopRight,
819+
float cornerSizeBottomRight,
820+
float cornerSizeBottomLeft) {
821+
shapeAppearanceModel.getTopLeftCorner().setCornerSize(cornerSizeTopLeft);
822+
shapeAppearanceModel.getTopRightCorner().setCornerSize(cornerSizeTopRight);
823+
shapeAppearanceModel.getBottomRightCorner().setCornerSize(cornerSizeBottomRight);
824+
shapeAppearanceModel.getBottomLeftCorner().setCornerSize(cornerSizeBottomLeft);
825+
}
826+
827+
private float adjustCornerSizeForStrokeSize(float cornerSize) {
828+
float adjustedCornerSize = cornerSize - getStrokeInsetLength();
829+
return Math.max(adjustedCornerSize, 0);
765830
}
766831

767832
private void prepareCanvasForShadow(Canvas canvas) {
@@ -791,7 +856,7 @@ private void prepareCanvasForShadow(Canvas canvas) {
791856
*/
792857
private void drawCompatShadow(Canvas canvas) {
793858
if (shadowCompatOffset != 0) {
794-
canvas.drawPath(pathInsetByStroke, shadowRenderer.getShadowPaint());
859+
canvas.drawPath(path, shadowRenderer.getShadowPaint());
795860
}
796861

797862
// Draw the fake shadow for each of the corners and edges.
@@ -806,7 +871,7 @@ private void drawCompatShadow(Canvas canvas) {
806871
int shadowOffsetY = (int) (shadowCompatOffset * Math.cos(Math.toRadians(shadowCompatRotation)));
807872

808873
canvas.translate(-shadowOffsetX, -shadowOffsetY);
809-
canvas.drawPath(pathInsetByStroke, clearPaint);
874+
canvas.drawPath(path, clearPaint);
810875
canvas.translate(shadowOffsetX, shadowOffsetY);
811876
}
812877

@@ -1049,20 +1114,21 @@ private boolean updateColorsForState(int[] state, boolean invalidateSelf) {
10491114
return invalidateSelf;
10501115
}
10511116

1052-
private RectF getBoundsInsetByStroke() {
1053-
RectF bounds = getBoundsAsRectF();
1054-
float strokeInsetWidth = getStrokeInsetWidth();
1055-
bounds.inset(strokeInsetWidth, strokeInsetWidth);
1056-
return bounds;
1057-
}
1058-
1059-
private float getStrokeInsetWidth() {
1117+
private float getStrokeInsetLength() {
10601118
if (hasStroke()) {
10611119
return strokePaint.getStrokeWidth() / 2.0f;
10621120
}
10631121
return 0f;
10641122
}
10651123

1124+
private RectF getBoundsInsetByStroke() {
1125+
RectF rectF = getBoundsAsRectF();
1126+
float inset = getStrokeInsetLength();
1127+
insetRectF.set(
1128+
rectF.left + inset, rectF.top + inset, rectF.right - inset, rectF.bottom - inset);
1129+
return insetRectF;
1130+
}
1131+
10661132
/**
10671133
* Dummy implementation of constant state. This drawable doesn't have shared state. Implementing
10681134
* so that calls to getConstantState().newDrawable() don't crash on L and M.

0 commit comments

Comments
 (0)