diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/component/Component.java b/renderer/native/android/src/main/java/com/tencent/renderer/component/Component.java index 73e66202029..9fcf62deb67 100644 --- a/renderer/native/android/src/main/java/com/tencent/renderer/component/Component.java +++ b/renderer/native/android/src/main/java/com/tencent/renderer/component/Component.java @@ -186,6 +186,17 @@ public Path getContentRegionPath() { return (mBackgroundDrawable != null) ? mBackgroundDrawable.getBorderPath() : null; } + /** + * Returns the resolved uniform border radius (in pixels) of the background, + * or {@code -1f} when there is no border radius or the four corners do not + * share the same radius. See + * {@link BackgroundDrawable#getUniformBorderRadius()} for the precise + * semantics. + */ + public float getUniformBorderRadius() { + return (mBackgroundDrawable != null) ? mBackgroundDrawable.getUniformBorderRadius() : -1f; + } + /** * Get background layer drawable * diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/component/FlatViewGroup.java b/renderer/native/android/src/main/java/com/tencent/renderer/component/FlatViewGroup.java index 1d9a6070a08..53496ff4288 100644 --- a/renderer/native/android/src/main/java/com/tencent/renderer/component/FlatViewGroup.java +++ b/renderer/native/android/src/main/java/com/tencent/renderer/component/FlatViewGroup.java @@ -18,7 +18,12 @@ import android.content.Context; import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.Rect; +import android.os.Build; +import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import com.tencent.mtt.hippy.uimanager.RenderManager; import com.tencent.mtt.hippy.utils.LogUtils; @@ -31,7 +36,15 @@ public class FlatViewGroup extends ViewGroup { private static final String TAG = "FlatViewGroup"; + private static final float TRANSFORM_EPSILON = 1e-4f; + private static final int MAX_TRANSFORM_ANCESTOR_DEPTH = 20; + // UI thread only: Android View drawing is expected to be single-threaded, so this + // static counter is only used to avoid nested offscreen layers within one draw pass. + private static int sOffscreenLayerDepth = 0; private final DispatchDrawHelper mDispatchDrawHelper = new DispatchDrawHelper(); + // Reused during draw() to avoid allocation. Safe under the same UI-thread-only + // drawing assumption as sOffscreenLayerDepth. + private final Rect mLayerBounds = new Rect(); public FlatViewGroup(Context context) { super(context); @@ -87,6 +100,72 @@ protected int getChildDrawingOrder(int childCount, int i) { return (index < 0 || index >= getChildCount()) ? i : index; } + private boolean hasAncestorOrSelfTransform() { + View view = this; + for (int depth = 0; view != null && depth < MAX_TRANSFORM_ANCESTOR_DEPTH; depth++) { + float sx = view.getScaleX(); + float sy = view.getScaleY(); + float rz = view.getRotation(); + float rx = view.getRotationX(); + float ry = view.getRotationY(); + if (Math.abs(sx - 1f) > TRANSFORM_EPSILON + || Math.abs(sy - 1f) > TRANSFORM_EPSILON + || Math.abs(rz) > TRANSFORM_EPSILON + || Math.abs(rx) > TRANSFORM_EPSILON + || Math.abs(ry) > TRANSFORM_EPSILON) { + return true; + } + ViewParent parent = view.getParent(); + if (parent instanceof View) { + view = (View) parent; + } else { + break; + } + } + return false; + } + + private boolean shouldDrawInOffscreenLayer() { + // Keep the ancestor transform walk on the old-HWUI fallback path only. Newer + // Android versions handle Canvas scaling for complex drawing operations, so + // they can short-circuit before hasAncestorOrSelfTransform(). + return sOffscreenLayerDepth == 0 + && getWidth() > 0 + && getHeight() > 0 + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1 + && hasAncestorOrSelfTransform(); + } + + @Override + public void draw(Canvas canvas) { + RenderNode node = RenderManager.getRenderNode(this); + if (node == null) { + super.draw(canvas); + return; + } + boolean useOffscreenLayer = shouldDrawInOffscreenLayer(); + int restoreCount = -1; + if (useOffscreenLayer) { + canvas.getClipBounds(mLayerBounds); + restoreCount = canvas.saveLayer(mLayerBounds.left, mLayerBounds.top, + mLayerBounds.right, mLayerBounds.bottom, null); + } + if (useOffscreenLayer) { + sOffscreenLayerDepth++; + } + try { + super.draw(canvas); + } finally { + if (restoreCount >= 0) { + canvas.restoreToCount(restoreCount); + } + if (useOffscreenLayer) { + sOffscreenLayerDepth--; + } + } + } + @Override protected void dispatchDraw(Canvas canvas) { RenderNode node = RenderManager.getRenderNode(this); @@ -95,23 +174,30 @@ protected void dispatchDraw(Canvas canvas) { return; } boolean clipChildren = getClipChildren(); - canvas.save(); + int restoreCount = -1; if (clipChildren) { Component component = node.getComponent(); - if (component != null && component.getContentRegionPath() != null) { - canvas.clipPath(component.getContentRegionPath()); + Path roundCornerClipPath = (component != null) ? component.getContentRegionPath() : null; + restoreCount = canvas.save(); + if (roundCornerClipPath != null) { + canvas.clipPath(roundCornerClipPath); } else { - canvas.clipRect(0, 0, getRight() - getLeft(), getBottom() - getTop()); + canvas.clipRect(0, 0, getWidth(), getHeight()); } } mDispatchDrawHelper.onDispatchDrawStart(canvas, node); - super.dispatchDraw(canvas); - if (mDispatchDrawHelper.isActive()) { - // Check the remaining non rendered sub nodes, behind the last sub node with host view - mDispatchDrawHelper.drawNext(this); + try { + super.dispatchDraw(canvas); + if (mDispatchDrawHelper.isActive()) { + // Check the remaining non rendered sub nodes, behind the last sub node with host view + mDispatchDrawHelper.drawNext(this); + } + } finally { + mDispatchDrawHelper.onDispatchDrawEnd(); + if (restoreCount >= 0) { + canvas.restoreToCount(restoreCount); + } } - mDispatchDrawHelper.onDispatchDrawEnd(); - canvas.restore(); } @Override @@ -129,4 +215,4 @@ protected void onDraw(Canvas canvas) { } } } -} +} \ No newline at end of file diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/component/drawable/BackgroundDrawable.java b/renderer/native/android/src/main/java/com/tencent/renderer/component/drawable/BackgroundDrawable.java index a416d41f940..8c8df7af924 100644 --- a/renderer/native/android/src/main/java/com/tencent/renderer/component/drawable/BackgroundDrawable.java +++ b/renderer/native/android/src/main/java/com/tencent/renderer/component/drawable/BackgroundDrawable.java @@ -23,6 +23,7 @@ import android.graphics.PathEffect; import android.graphics.RectF; import android.graphics.Region; +import android.os.Build; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -117,6 +118,26 @@ public Path getBorderPath() { return mResolvedInfo.hasBorderRadius ? mResolvedInfo.borderOutsidePath : null; } + /** + * Returns the resolved border radius shared by all four corners (in pixels), + * or {@code -1f} when there is no border radius or the four corners do not + * share the same radius. + * + *
This is the radius after the BorderResolvedInfo resolve step + * has potentially scaled the configured radii to fit within the rect bounds, + * so callers that combine this value with the rect must use the same + * rect that was passed into {@link #updatePath()} ({@code mRect}). + * + *
Mainly intended to let parent {@code ViewGroup}s install a uniform
+ * round-rect {@link android.graphics.Outline} for clip-to-outline based
+ * rounded-corner clipping, which is unaffected by the older-HWUI
+ * {@code clipPath}-under-ancestor-scale issue.
+ */
+ public float getUniformBorderRadius() {
+ updatePath();
+ return mResolvedInfo.getUniformBorderRadius();
+ }
+
protected void updatePath() {
if (!mUpdatePathRequired) {
return;
@@ -143,8 +164,35 @@ protected void drawBackgroundColor(@NonNull Canvas canvas) {
}
if (mResolvedInfo.hasBorderRadius) {
- assert mResolvedInfo.borderOutsidePath != null;
- canvas.drawPath(mResolvedInfo.borderOutsidePath, paint);
+ // Prefer Canvas.drawRoundRect over Canvas.drawPath when the four corners
+ // share the same radius.
+ //
+ // drawRoundRect maps to a dedicated HWUI primitive on all supported API
+ // levels and is recorded as a vector command in the parent RenderNode's
+ // display list, so any ancestor transform (e.g. transform: scale()) is
+ // applied as a matrix transform at draw time and the result remains
+ // crisp at any scale.
+ //
+ // drawPath, in contrast, falls under the HWUI Canvas-scaling
+ // limitation documented for complex drawing operations before API
+ // 28: the hardware-accelerated 2D pipeline may rasterize such
+ // operations at scale 1.0 and then let the GPU scale that texture,
+ // which can degrade quality under ancestor transforms. Android's
+ // hardware acceleration docs list drawPath / complex shapes as
+ // scaling correctly starting from API 28. We therefore prefer
+ // drawRoundRect whenever the geometry can be expressed as a
+ // uniform-radius round rect.
+ float uniformRadius = mResolvedInfo.getUniformBorderRadius();
+ if (uniformRadius >= 0) {
+ canvas.drawRoundRect(mRect, uniformRadius, uniformRadius, paint);
+ } else {
+ // Non-uniform per-corner radii cannot be expressed by drawRoundRect;
+ // fall back to the pre-built rounded-rect Path here. This branch may
+ // still be affected by the older-HWUI drawPath issue described
+ // above, but it is rarely hit in practice.
+ assert mResolvedInfo.borderOutsidePath != null;
+ canvas.drawPath(mResolvedInfo.borderOutsidePath, paint);
+ }
} else {
canvas.drawRect(mRect, paint);
}
@@ -187,7 +235,108 @@ protected void drawBorder(@NonNull Canvas canvas) {
if (!mResolvedInfo.hasVisibleBorder) {
return;
}
- canvas.save();
+ // Fast path: the four sides share the same width / color / style (i.e.
+ // !drawBorderSideBySide) AND the four corners share the same resolved
+ // radius. Under these conditions the border can be expressed exactly as
+ // a single stroked round rect along the geometric midline of the band
+ // between borderOutsidePath and borderInsidePath, which lets us emit
+ // the border as one Canvas.drawRoundRect command.
+ //
+ // Why a fast path is needed:
+ //
+ // The general path below relies on
+ // canvas.clipPath(borderOutsidePath);
+ // canvas.clipPath(borderInsidePath, Region.Op.DIFFERENCE);
+ // canvas.drawPath(borderMidlinePath, STROKE);
+ // to confine the stroke to the border band. This is a complex Canvas
+ // operation built from path clipping and drawPath. Android's hardware
+ // acceleration docs state that before API 28, some drawing operations
+ // were implemented as scale-1.0 textures and then scaled by the GPU,
+ // causing quality degradation at high scale; the same docs list
+ // drawPath / complex shapes as scaling correctly starting from API 28.
+ // In this path, once the RenderNode's content has been rasterized at
+ // its natural size, an ancestor transform such as transform: scale(1.5)
+ // can only resample that raster, producing visibly blurry / aliased
+ // corners and a border that does not appear to scale together with its
+ // parent.
+ //
+ // Canvas.drawRoundRect, by contrast, is a first-class HWUI primitive
+ // on every supported API level and is recorded into the display list
+ // as a vector command, so it is unaffected by the issue above.
+ if (!mResolvedInfo.drawBorderSideBySide
+ && mResolvedInfo.hasBorderRadius
+ && mResolvedInfo.hasContentRadius
+ && mResolvedInfo.borderMidlineRect != null) {
+ float uniformRadius = mResolvedInfo.getUniformBorderRadius();
+ if (uniformRadius >= 0) {
+ assert mPaint != null;
+ float strokeWidth = mResolvedInfo.strokeWidth.left;
+ // Geometry of the fast path:
+ // - borderMidlineRect is the outer rect inset by borderWidth/2
+ // on each side, i.e. the centerline of the border band.
+ // - With strokeWidth = borderWidth, the STROKE expands by
+ // borderWidth/2 on each side of the midline, so the outer
+ // edge of the stroke lands exactly on borderOutsidePath
+ // and the inner edge lands exactly on borderInsidePath.
+ // - The midline radius is therefore (uniformRadius -
+ // borderWidth/2). Within this fast path we already require
+ // !drawBorderSideBySide (equal widths on all four sides)
+ // and a uniform corner radius, under which the
+ // hasContentRadius predicate (see
+ // BorderResolvedInfo#hasContentRadius) reduces to
+ // uniformRadius > borderWidth, so midlineRadius is
+ // strictly positive. The Math.max(0f, ...) is purely
+ // defensive against floating-point rounding when the two
+ // values are extremely close.
+ // The stroked round rect therefore tiles exactly the band
+ // that the general path would have produced via clipPath,
+ // with no clipping required.
+ float midlineRadius = Math.max(0f, uniformRadius - strokeWidth * 0.5f);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(strokeWidth);
+ mPaint.setColor(mResolvedInfo.borderColor.left);
+ mPaint.setPathEffect(mResolvedInfo.pathEffect.left);
+ canvas.drawRoundRect(mResolvedInfo.borderMidlineRect,
+ midlineRadius, midlineRadius, mPaint);
+ return;
+ }
+ }
+ if (mResolvedInfo.drawBorderSideBySide && shouldDrawSideBySideBorderAsFill()) {
+ drawBorderSideFillInternal(canvas,
+ mResolvedInfo.strokeWidth.left,
+ mResolvedInfo.borderColor.left,
+ mResolvedInfo.borderSideFill.left);
+ drawBorderSideFillInternal(canvas,
+ mResolvedInfo.strokeWidth.top,
+ mResolvedInfo.borderColor.top,
+ mResolvedInfo.borderSideFill.top);
+ drawBorderSideFillInternal(canvas,
+ mResolvedInfo.strokeWidth.right,
+ mResolvedInfo.borderColor.right,
+ mResolvedInfo.borderSideFill.right);
+ drawBorderSideFillInternal(canvas,
+ mResolvedInfo.strokeWidth.bottom,
+ mResolvedInfo.borderColor.bottom,
+ mResolvedInfo.borderSideFill.bottom);
+ return;
+ }
+ // General path: per-side widths / colors / styles, non-uniform corner
+ // radii, or no rounded corners at all. Falls back to the historical
+ // clipPath + drawPath implementation. On older HWUI versions this path
+ // may still trigger the offscreen-rasterization behavior described
+ // above. When a rounded complex border cannot use the drawRoundRect
+ // fast path, explicitly isolate the border drawing into a layer on
+ // older Android versions (API 27 and below) so the outer and inner path
+ // clips are applied in the same local rasterization step. API 28 is the
+ // cutoff because Android's hardware acceleration docs state that Canvas
+ // scaling for all drawing operations works from API 28 onward, and list
+ // drawPath / complex shapes as scaling correctly starting from API 28.
+ boolean useLayer = shouldDrawComplexRoundedBorderInLayer();
+ if (useLayer) {
+ canvas.saveLayer(mRect, null);
+ } else {
+ canvas.save();
+ }
if (mResolvedInfo.hasBorderRadius) {
canvas.clipPath(mResolvedInfo.borderOutsidePath);
if (mResolvedInfo.hasContentRadius) {
@@ -234,6 +383,42 @@ protected void drawBorder(@NonNull Canvas canvas) {
canvas.restore();
}
+ private boolean shouldDrawComplexRoundedBorderInLayer() {
+ // O_MR1 is API 27, so this fallback applies only before the official
+ // API 28 Canvas-scaling boundary documented by Android.
+ return mResolvedInfo.hasBorderRadius && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1;
+ }
+
+ private boolean shouldDrawSideBySideBorderAsFill() {
+ return canDrawBorderSideAsFill(mResolvedInfo.strokeWidth.left,
+ mResolvedInfo.pathEffect.left, mResolvedInfo.borderSideFill.left)
+ && canDrawBorderSideAsFill(mResolvedInfo.strokeWidth.top,
+ mResolvedInfo.pathEffect.top, mResolvedInfo.borderSideFill.top)
+ && canDrawBorderSideAsFill(mResolvedInfo.strokeWidth.right,
+ mResolvedInfo.pathEffect.right, mResolvedInfo.borderSideFill.right)
+ && canDrawBorderSideAsFill(mResolvedInfo.strokeWidth.bottom,
+ mResolvedInfo.pathEffect.bottom, mResolvedInfo.borderSideFill.bottom);
+ }
+
+ private boolean canDrawBorderSideAsFill(int strokeWidth, @Nullable PathEffect pathEffect,
+ @Nullable Path fillPath) {
+ // Only solid borders can be represented by the pre-filled side path;
+ // dashed / dotted borders must keep the stroked pathEffect flow.
+ return strokeWidth <= 0 || (pathEffect == null && fillPath != null);
+ }
+
+ private void drawBorderSideFillInternal(@NonNull Canvas canvas, int strokeWidth, int color,
+ @Nullable Path fillPath) {
+ if (strokeWidth <= 0 || fillPath == null) {
+ return;
+ }
+ assert mPaint != null;
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(color);
+ mPaint.setPathEffect(null);
+ canvas.drawPath(fillPath, mPaint);
+ }
+
private void drawBorderSideInternal(@NonNull Canvas canvas, int strokeWidth, int color,
@Nullable PathEffect pathEffect, Path borderPath, Path clipPath) {
if (strokeWidth <= 0) {
@@ -405,6 +590,8 @@ public void setBorderWith(@Px int width, BorderSide side) {
public static class BorderRadius {
+ private static final float RADIUS_EPSILON = 1e-4f;
+
public float topLeft;
public float topRight;
public float bottomRight;
@@ -441,6 +628,12 @@ public void setBorderRadius(@Px float radius, BorderArc arc) {
LogUtils.w(TAG, "setBorderRadius: Unknown arc: " + arc);
}
}
+
+ public boolean hasSameRadiusOnAllSides() {
+ return Math.abs(topLeft - topRight) <= RADIUS_EPSILON
+ && Math.abs(topRight - bottomRight) <= RADIUS_EPSILON
+ && Math.abs(bottomRight - bottomLeft) <= RADIUS_EPSILON;
+ }
}
public static class BorderColor {
@@ -530,4 +723,4 @@ public void setBorderStyle(BorderStyle style, BorderSide side) {
}
}
}
-}
+}
\ No newline at end of file
diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/component/drawable/BorderResolvedInfo.java b/renderer/native/android/src/main/java/com/tencent/renderer/component/drawable/BorderResolvedInfo.java
index 2be592d7c47..f3fbfc8f256 100644
--- a/renderer/native/android/src/main/java/com/tencent/renderer/component/drawable/BorderResolvedInfo.java
+++ b/renderer/native/android/src/main/java/com/tencent/renderer/component/drawable/BorderResolvedInfo.java
@@ -54,6 +54,7 @@ protected float[] initialValue() {
final BorderColor borderColor = new BorderColor(Color.TRANSPARENT);
final BorderSideValue The value returned here is the radius after {@link
+ * #resolveBorderRadius(float, float, float, BackgroundDrawable.BorderRadius)}
+ * has potentially scaled the user-supplied radii down so that
+ * {@code (left + right)} and {@code (top + bottom)} corner radii fit within
+ * the rect bounds. Callers that draw geometry based on this value must
+ * therefore use it together with the resolved {@code mRect} / {@code
+ * borderWidth}, not with the raw values configured on {@code
+ * BackgroundDrawable}.
+ *
+ * This method must be called after {@link #resolve}; otherwise
+ * the returned value reflects stale state from the previous resolve cycle.
+ */
+ float getUniformBorderRadius() {
+ if (!hasBorderRadius) {
+ return -1f;
+ }
+ if (borderRadius.hasSameRadiusOnAllSides()) {
+ return borderRadius.topLeft;
+ }
+ return -1f;
+ }
+
void resolve(RectF rect, int preferBorderWidth, BorderWidth preferBorderWidths, float preferBorderRadius,
BorderRadius preferBorderRadii, int preferBorderColor, BorderColor preferBorderColors,
BorderStyle preferBorderStyle, BorderStyles preferBorderStyles) {
@@ -400,6 +428,7 @@ private void updateBorderStrokeLeft(RectF rect) {
}
clip.lineTo(rect.left, rect.bottom);
clip.close();
+ updateBorderSideFill(BorderSide.LEFT, clip);
strokeWidth.left = strokeSize;
pathEffect.left = buildPathEffect(style, borderWidth.left, (int) strokeLength, roundStart, roundEnd);
}
@@ -458,7 +487,7 @@ private void updateBorderStrokeTop(RectF rect) {
if (endRadius > borderWidth.top && endRadius > borderWidth.left) {
float endDiameter = 2 * endRadius;
// top-left inside arc
- float endAngle = 90 - arcAngle(startRadius, borderWidth.left, borderWidth.top, 1);
+ float endAngle = 90 - arcAngle(endRadius, borderWidth.left, borderWidth.top, 1);
clip.arcTo(
rect.left + borderWidth.left,
rect.top + borderWidth.top,
@@ -467,7 +496,7 @@ private void updateBorderStrokeTop(RectF rect) {
270, -endAngle, false);
// top-left midline arc
float endMidlineAngle = style != BorderStyle.DOTTED ? 90
- : 90 - arcAngle(startRadius, borderWidth.left, borderWidth.top, 0.5f);
+ : 90 - arcAngle(endRadius, borderWidth.left, borderWidth.top, 0.5f);
midline.arcTo(
rect.left + borderWidth.left * 0.5f,
rect.top + borderWidth.top * 0.5f,
@@ -487,6 +516,7 @@ private void updateBorderStrokeTop(RectF rect) {
}
clip.lineTo(rect.left, rect.top);
clip.close();
+ updateBorderSideFill(BorderSide.TOP, clip);
strokeWidth.top = strokeSize;
pathEffect.top = buildPathEffect(style, borderWidth.top, (int) strokeLength, roundStart, roundEnd);
}
@@ -546,7 +576,7 @@ private void updateBorderStrokeRight(RectF rect) {
if (endRadius > borderWidth.top && endRadius > borderWidth.right) {
float endDiameter = 2 * endRadius;
// top-right inside arc
- float endAngle = 90 - arcAngle(startRadius, borderWidth.top, borderWidth.right, 1);
+ float endAngle = 90 - arcAngle(endRadius, borderWidth.top, borderWidth.right, 1);
clip.arcTo(
rect.right - endDiameter + borderWidth.right,
rect.top + borderWidth.top,
@@ -555,7 +585,7 @@ private void updateBorderStrokeRight(RectF rect) {
0, -endAngle, false);
// top-right midline arc
float endMidlineAngle = style != BorderStyle.DOTTED ? 90
- : 90 - arcAngle(startRadius, borderWidth.top, borderWidth.right, 0.5f);
+ : 90 - arcAngle(endRadius, borderWidth.top, borderWidth.right, 0.5f);
midline.arcTo(
rect.right - endDiameter + borderWidth.right * 0.5f,
rect.top + borderWidth.top * 0.5f,
@@ -575,6 +605,7 @@ private void updateBorderStrokeRight(RectF rect) {
}
clip.lineTo(rect.right, rect.top);
clip.close();
+ updateBorderSideFill(BorderSide.RIGHT, clip);
strokeWidth.right = strokeSize;
pathEffect.right = buildPathEffect(style, borderWidth.right, (int) strokeLength, roundStart, roundEnd);
}
@@ -634,7 +665,7 @@ private void updateBorderStrokeBottom(RectF rect) {
if (endRadius > borderWidth.bottom && endRadius > borderWidth.right) {
float endDiameter = 2 * endRadius;
// bottom-right inside arc
- float endAngle = 90 - arcAngle(startRadius, borderWidth.right, borderWidth.bottom, 1);
+ float endAngle = 90 - arcAngle(endRadius, borderWidth.right, borderWidth.bottom, 1);
clip.arcTo(
rect.right - endDiameter + borderWidth.right,
rect.bottom - endDiameter + borderWidth.bottom,
@@ -643,7 +674,7 @@ private void updateBorderStrokeBottom(RectF rect) {
90, -endAngle, false);
// bottom-right midline arc
float endMidlineAngle = style != BorderStyle.DOTTED ? 90
- : 90 - arcAngle(startRadius, borderWidth.right, borderWidth.bottom, 0.5f);
+ : 90 - arcAngle(endRadius, borderWidth.right, borderWidth.bottom, 0.5f);
midline.arcTo(
rect.right - endDiameter + borderWidth.right * 0.5f,
rect.bottom - endDiameter + borderWidth.bottom * 0.5f,
@@ -663,10 +694,47 @@ private void updateBorderStrokeBottom(RectF rect) {
}
clip.lineTo(rect.right, rect.bottom);
clip.close();
+ updateBorderSideFill(BorderSide.BOTTOM, clip);
strokeWidth.bottom = strokeSize;
pathEffect.bottom = buildPathEffect(style, borderWidth.bottom, (int) strokeLength, roundStart, roundEnd);
}
+ private void updateBorderSideFill(BorderSide side, Path sideClip) {
+ Path sideFill;
+ switch (side) {
+ case LEFT:
+ sideFill = borderSideFill.left;
+ if (sideFill == null) {
+ borderSideFill.left = sideFill = new Path();
+ }
+ break;
+ case TOP:
+ sideFill = borderSideFill.top;
+ if (sideFill == null) {
+ borderSideFill.top = sideFill = new Path();
+ }
+ break;
+ case RIGHT:
+ sideFill = borderSideFill.right;
+ if (sideFill == null) {
+ borderSideFill.right = sideFill = new Path();
+ }
+ break;
+ case BOTTOM:
+ sideFill = borderSideFill.bottom;
+ if (sideFill == null) {
+ borderSideFill.bottom = sideFill = new Path();
+ }
+ break;
+ default:
+ return;
+ }
+ sideFill.set(sideClip);
+ if (hasBorderRadius && borderOutsidePath != null) {
+ sideFill.op(borderOutsidePath, Path.Op.INTERSECT);
+ }
+ }
+
private static boolean hasContentRadius(BorderWidth borderWidth, BorderRadius borderRadius) {
return borderRadius.topLeft > borderWidth.left || borderRadius.topLeft > borderWidth.top
|| borderRadius.topRight > borderWidth.top || borderRadius.topRight > borderWidth.right