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 borderSideClip = new BorderSideValue<>(); final BorderSideValue borderSideMidline = new BorderSideValue<>(); + final BorderSideValue borderSideFill = new BorderSideValue<>(); final BorderSideValue pathEffect = new BorderSideValue<>(); boolean hasVisibleBorder; boolean hasBorderRadius; @@ -67,6 +68,33 @@ protected float[] initialValue() { private final BorderRadius borderRadius = new BorderRadius(0); private final BorderStyles borderStyles = new BorderStyles(BorderStyle.NONE); + /** + * Returns the resolved border radius, in pixels, shared by all four corners, + * or {@code -1f} when there is no border radius at all or when the four + * corners do not all share the same radius. + * + *

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