Add StrokeCap shadow implementation to ArcLine.
BUG: 307506725
Relnote: Added renderer support for StrokeCap Shadow.
Change-Id: I48b177194ede8fb8d365cab01998c89e0a2b44cd
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java
index 0fc61ed..8e99e07 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java
@@ -90,6 +90,7 @@
@Nullable private ArcDrawable mArcDrawable;
@NonNull private Cap mCapStyle;
+ @Nullable private StrokeCapShadow mCapShadow;
public WearCurvedLineView(@NonNull Context context) {
this(context, null);
@@ -157,15 +158,15 @@
getMeasuredHeight() - insetPx);
float clampedSweepAngle = resolveSweepAngleDegrees();
- if (mSweepGradientHelper != null) {
+ if (mSweepGradientHelper != null || mCapShadow != null) {
mArcDrawable =
new ArcDrawableImpl(
bounds,
clampedSweepAngle,
mThicknessPx,
- mCapStyle,
basePaint,
- mSweepGradientHelper);
+ mSweepGradientHelper,
+ mCapShadow);
} else {
mArcDrawable = new ArcDrawableLegacy(bounds, clampedSweepAngle, basePaint);
}
@@ -286,6 +287,16 @@
mCapStyle = cap;
}
+ /** Sets the parameters for the stroke cap shadow. */
+ public void setStrokeCapShadow(float blurRadius, int color) {
+ this.mCapShadow = new StrokeCapShadow(blurRadius, color);
+ }
+
+ /** Clears the stroke cap shadow. */
+ public void clearStrokeCapShadow() {
+ this.mCapShadow = null;
+ }
+
@Override
protected void onDraw(@NonNull Canvas canvas) {
if (mArcDrawable == null) {
@@ -549,8 +560,11 @@
@NonNull private final Paint mPaint;
@NonNull private final Path mPath;
- /** A path to be clipped out when drawing, in order to exclude one of the stroke caps. */
- @Nullable private Path mClipOutPath = null;
+ /** A region to be clipped out when drawing, in order to exclude one of the stroke caps. */
+ @Nullable private Path mExcludedCapRegion = null;
+
+ /** A region to be clipped in when drawing, in order to only include this region. */
+ @Nullable private Path mMaskRegion = null;
/** Creates a line segment that forms a full circle. */
static ArcSegment circle(@NonNull RectF bounds, @NonNull Paint paint) {
@@ -583,12 +597,28 @@
return new ArcSegment(line, paint);
}
+ /** A segment that draws the shadow layer that matches the path of the given segment. */
+ static ArcSegment strokeCapShadowLayer(
+ @NonNull RectF bounds,
+ float lineThicknessPx,
+ @NonNull ArcSegment segment,
+ @NonNull Paint paint) {
+ // Use a mask to only include the region between inner and outer bounds of the arc line.
+ // The Paint's shadow layer will draw the shadow in all directions around the stroke but
+ // we only want the part in the direction of the Cap to be visible.
+ Path maskRegion = new Path();
+ RectF innerBounds = shrinkRectF(bounds, lineThicknessPx / 2f);
+ RectF outerBounds = expandRectF(bounds, lineThicknessPx / 2f);
+ maskRegion.addOval(innerBounds, Direction.CW);
+ maskRegion.addOval(outerBounds, Direction.CCW);
+ return new ArcSegment(segment.mPath, paint, maskRegion, segment.mExcludedCapRegion);
+ }
+
ArcSegment(
@NonNull RectF bounds,
float startAngle,
float sweepAngle,
float thicknessPx,
- @NonNull Cap capStyle,
@NonNull CapPosition capPosition,
@NonNull Paint paint) {
if (Math.abs(sweepAngle) > 180f) {
@@ -607,49 +637,73 @@
}
// If a single cap is present, we clip out the Cap that should not be included.
- if (capPosition != CapPosition.NONE && capStyle != Cap.BUTT) {
- float centerX = (bounds.left + bounds.right) / 2f;
- float centerY = (bounds.top + bounds.bottom) / 2f;
- RectF clipRectBounds =
- new RectF(
- bounds.left - thicknessPx,
- bounds.top - thicknessPx,
- bounds.right + thicknessPx,
- bounds.bottom + thicknessPx);
+ if (capPosition != CapPosition.NONE) {
- mClipOutPath = new Path();
- mClipOutPath.moveTo(centerX, centerY);
+ RectF clipRectBounds = expandRectF(bounds, thicknessPx);
+
+ mExcludedCapRegion = new Path();
+ mExcludedCapRegion.moveTo(clipRectBounds.centerX(), clipRectBounds.centerY());
float sweepDirection = Math.signum(sweepAngle);
+
if (capPosition == CapPosition.START) {
// Clip out END of segment.
- mClipOutPath.arcTo(
+ mExcludedCapRegion.arcTo(
clipRectBounds,
startAngle + sweepAngle,
sweepDirection * CLIP_OUT_PATH_SPAN_DEGREES);
} else if (capPosition == CapPosition.END) {
// Clip out START of segment.
- mClipOutPath.arcTo(
+ mExcludedCapRegion.arcTo(
clipRectBounds,
startAngle,
-sweepDirection * CLIP_OUT_PATH_SPAN_DEGREES);
}
- mClipOutPath.close();
+ mExcludedCapRegion.close();
}
}
- ArcSegment(@NonNull Path mainPath, @NonNull Paint paint) {
+ ArcSegment(
+ @NonNull Path mainPath,
+ @NonNull Paint paint,
+ @Nullable Path maskRegion,
+ @Nullable Path excludedCapRegion) {
this.mPath = mainPath;
this.mPaint = paint;
+ this.mMaskRegion = maskRegion;
+ this.mExcludedCapRegion = excludedCapRegion;
+ }
+
+ ArcSegment(@NonNull Path mainPath, @NonNull Paint paint) {
+ this(mainPath, paint, /* maskRegion= */ null, /* excludedCapRegion= */ null);
}
public void onDraw(@NonNull Canvas canvas) {
canvas.save();
- if (mClipOutPath != null) {
- canvas.clipOutPath(mClipOutPath);
+ if (mExcludedCapRegion != null) {
+ canvas.clipOutPath(mExcludedCapRegion);
+ }
+ if (mMaskRegion != null) {
+ canvas.clipPath(mMaskRegion);
}
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
+
+ /**
+ * Returns a new rectangle, expanding the given bounds by {@code offset} in all directions.
+ * Use a negative offset value to shrink the original bounds.
+ */
+ static RectF expandRectF(@NonNull RectF bounds, float offset) {
+ return new RectF(
+ bounds.left - offset,
+ bounds.top - offset,
+ bounds.right + offset,
+ bounds.bottom + offset);
+ }
+
+ static RectF shrinkRectF(@NonNull RectF bounds, float offset) {
+ return expandRectF(bounds, -offset);
+ }
}
/**
@@ -679,9 +733,9 @@
@NonNull RectF bounds,
float sweepAngle,
float thicknessPx,
- @NonNull Cap capStyle,
@NonNull Paint basePaint,
- @Nullable SweepGradientHelper sweepGradHelper) {
+ @Nullable SweepGradientHelper sweepGradHelper,
+ @Nullable StrokeCapShadow strokeCapShadow) {
if (Math.abs(sweepAngle) == 0f) {
return;
}
@@ -721,6 +775,17 @@
float segmentSweep = topLayerLength / 2f;
+ @Nullable Paint shadowPaint = null;
+ if (strokeCapShadow != null) {
+ shadowPaint = new Paint(basePaint);
+ shadowPaint.setColor(Color.TRANSPARENT);
+ shadowPaint.setShadowLayer(
+ strokeCapShadow.mBlurRadius,
+ /* dx= */ 0f,
+ /* dy= */ 0f,
+ strokeCapShadow.mColor);
+ }
+
// Tail Segment.
Paint tailPaint = new Paint(basePaint);
if (sweepGradHelper != null) {
@@ -733,15 +798,22 @@
tailCapPosition);
tailPaint.setShader(shader);
}
- mSegments.add(
+ ArcSegment tailSegment =
new ArcSegment(
bounds,
drawStartAngle,
segmentSweep,
thicknessPx,
- capStyle,
tailCapPosition,
- tailPaint));
+ tailPaint);
+
+ // Add a shadow layer to the tail Cap if needed.
+ if (tailCapPosition != CapPosition.NONE && shadowPaint != null) {
+ mSegments.add(
+ ArcSegment.strokeCapShadowLayer(
+ bounds, thicknessPx, tailSegment, shadowPaint));
+ }
+ mSegments.add(tailSegment);
// Head Segment.
float midCursor = topLayerStartCursor + segmentSweep;
@@ -757,15 +829,22 @@
ArcSegment.CapPosition.END);
headPaint.setShader(shader);
}
- mSegments.add(
+ ArcSegment headSegment =
new ArcSegment(
bounds,
drawMidAngle,
segmentSweep,
thicknessPx,
- capStyle,
ArcSegment.CapPosition.END,
- headPaint));
+ headPaint);
+
+ // Add a shadow layer to the head Cap if needed.
+ if (shadowPaint != null) {
+ mSegments.add(
+ ArcSegment.strokeCapShadowLayer(
+ bounds, thicknessPx, headSegment, shadowPaint));
+ }
+ mSegments.add(headSegment);
// Fix discontinuity caused by anti-alias layer between Tail and Head. This is an arc
// with length equivalent to 1px.
@@ -819,4 +898,15 @@
/** Called when the arc should be drawn on the canvas. */
void onDraw(@NonNull Canvas canvas);
}
+
+ /** Data holder for the stroke cap shadow. */
+ private static final class StrokeCapShadow {
+ final float mBlurRadius;
+ final int mColor;
+
+ StrokeCapShadow(float blurRadius, int color) {
+ this.mBlurRadius = blurRadius;
+ this.mColor = color;
+ }
+ }
}