Implement border(Left|Right|Top|Bottom)Color for rounded borders

Reviewed By: achen1

Differential Revision: D6009609

fbshipit-source-id: 6a247a14aa48ac2edc637bf064216407a3a9a81f
This commit is contained in:
Ramanpreet Nara 2017-10-18 19:29:33 -07:00 committed by Facebook Github Bot
parent 4994d6a389
commit f7888310d4
2 changed files with 470 additions and 63 deletions

View File

@ -124,6 +124,44 @@ var styles = StyleSheet.create({
borderBottomColor: 'transparent',
borderLeftColor: 'red',
},
border12: {
borderStyle: 'solid',
overflow: 'hidden',
borderTopWidth: 10,
borderRightWidth: 20,
borderBottomWidth: 30,
borderLeftWidth: 40,
borderRadius: 20,
},
border13: {
borderStyle: 'solid',
overflow: 'hidden',
borderTopWidth: 10,
borderRightWidth: 20,
borderBottomWidth: 30,
borderLeftWidth: 40,
borderTopColor: 'red',
borderRightColor: 'green',
borderBottomColor: 'blue',
borderLeftColor: 'magenta',
borderRadius: 20,
},
border14: {
borderStyle: 'solid',
overflow: 'hidden',
borderTopWidth: 10,
borderRightWidth: 20,
borderBottomWidth: 30,
borderLeftWidth: 40,
borderTopColor: 'red',
borderRightColor: 'green',
borderBottomColor: 'blue',
borderLeftColor: 'magenta',
borderTopLeftRadius: 10,
borderTopRightRadius: 40,
borderBottomRightRadius: 30,
borderBottomLeftRadius: 40,
}
});
exports.title = 'Border';
@ -228,4 +266,25 @@ exports.examples = [
return <View style={[styles.border11]} />;
}
},
{
title: 'Curved border(Left|Right|Bottom|Top)Width',
description: 'Make a non-uniform width curved border',
render() {
return <View style={[styles.box, styles.border12]} />;
}
},
{
title: 'Curved border(Left|Right|Bottom|Top)Color',
description: 'Make a non-uniform color curved border',
render() {
return <View style={[styles.box, styles.border13]} />;
}
},
{
title: 'Curved border(Top|Bottom)(Left|Right)Radius',
description: 'Make a non-uniform radius curved border',
render() {
return <View style={[styles.box, styles.border14]} />;
}
}
];

View File

@ -17,6 +17,7 @@ import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
@ -91,6 +92,10 @@ public class ReactViewBackgroundDrawable extends Drawable {
private @Nullable RectF mInnerClipTempRectForBorderRadius;
private @Nullable RectF mOuterClipTempRectForBorderRadius;
private @Nullable RectF mTempRectForBorderRadiusOutline;
private @Nullable PointF mInnerTopLeftCorner;
private @Nullable PointF mInnerTopRightCorner;
private @Nullable PointF mInnerBottomRightCorner;
private @Nullable PointF mInnerBottomLeftCorner;
private boolean mNeedUpdatePathForBorderRadius = false;
private float mBorderRadius = YogaConstants.UNDEFINED;
@ -293,14 +298,73 @@ public class ReactViewBackgroundDrawable extends Drawable {
|| borderBottomWidth > 0
|| borderLeftWidth > 0
|| borderRightWidth > 0) {
int borderColor = getFullBorderColor();
mPaint.setColor(ColorUtil.multiplyColorAlpha(borderColor, mAlpha));
mPaint.setStyle(Paint.Style.FILL);
// Draw border
canvas.clipPath(mOuterClipPathForBorderRadius, Region.Op.INTERSECT);
canvas.clipPath(mInnerClipPathForBorderRadius, Region.Op.DIFFERENCE);
canvas.drawRect(getBounds(), mPaint);
final int colorLeft = getBorderColor(Spacing.LEFT);
final int colorTop = getBorderColor(Spacing.TOP);
final int colorRight = getBorderColor(Spacing.RIGHT);
final int colorBottom = getBorderColor(Spacing.BOTTOM);
final float left = mOuterClipTempRectForBorderRadius.left;
final float right = mOuterClipTempRectForBorderRadius.right;
final float top = mOuterClipTempRectForBorderRadius.top;
final float bottom = mOuterClipTempRectForBorderRadius.bottom;
if (borderLeftWidth > 0) {
final float x1 = left;
final float y1 = top;
final float x2 = mInnerTopLeftCorner.x;
final float y2 = mInnerTopLeftCorner.y;
final float x3 = mInnerBottomLeftCorner.x;
final float y3 = mInnerBottomLeftCorner.y;
final float x4 = left;
final float y4 = bottom;
drawQuadrilateral(canvas, colorLeft, x1, y1, x2, y2, x3, y3, x4, y4);
}
if (borderTopWidth > 0) {
final float x1 = left;
final float y1 = top;
final float x2 = mInnerTopLeftCorner.x;
final float y2 = mInnerTopLeftCorner.y;
final float x3 = mInnerTopRightCorner.x;
final float y3 = mInnerTopRightCorner.y;
final float x4 = right;
final float y4 = top;
drawQuadrilateral(canvas, colorTop, x1, y1, x2, y2, x3, y3, x4, y4);
}
if (borderRightWidth > 0) {
final float x1 = right;
final float y1 = top;
final float x2 = mInnerTopRightCorner.x;
final float y2 = mInnerTopRightCorner.y;
final float x3 = mInnerBottomRightCorner.x;
final float y3 = mInnerBottomRightCorner.y;
final float x4 = right;
final float y4 = bottom;
drawQuadrilateral(canvas, colorRight, x1, y1, x2, y2, x3, y3, x4, y4);
}
if (borderBottomWidth > 0) {
final float x1 = left;
final float y1 = bottom;
final float x2 = mInnerBottomLeftCorner.x;
final float y2 = mInnerBottomLeftCorner.y;
final float x3 = mInnerBottomRightCorner.x;
final float y3 = mInnerBottomRightCorner.y;
final float x4 = right;
final float y4 = bottom;
drawQuadrilateral(canvas, colorBottom, x1, y1, x2, y2, x3, y3, x4, y4);
}
}
canvas.restore();
@ -366,17 +430,27 @@ public class ReactViewBackgroundDrawable extends Drawable {
final float bottomRightRadius =
getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.BOTTOM_RIGHT);
final float innerTopLeftRadiusX = Math.max(topLeftRadius - borderLeftWidth, 0);
final float innerTopLeftRadiusY = Math.max(topLeftRadius - borderTopWidth, 0);
final float innerTopRightRadiusX = Math.max(topRightRadius - borderRightWidth, 0);
final float innerTopRightRadiusY = Math.max(topRightRadius - borderTopWidth, 0);
final float innerBottomRightRadiusX = Math.max(bottomRightRadius - borderRightWidth, 0);
final float innerBottomRightRadiusY = Math.max(bottomRightRadius - borderBottomWidth, 0);
final float innerBottomLeftRadiusX = Math.max(bottomLeftRadius - borderLeftWidth, 0);
final float innerBottomLeftRadiusY = Math.max(bottomLeftRadius - borderBottomWidth, 0);
mInnerClipPathForBorderRadius.addRoundRect(
mInnerClipTempRectForBorderRadius,
new float[] {
Math.max(topLeftRadius - borderLeftWidth, 0),
Math.max(topLeftRadius - borderTopWidth, 0),
Math.max(topRightRadius - borderRightWidth, 0),
Math.max(topRightRadius - borderTopWidth, 0),
Math.max(bottomRightRadius - borderRightWidth, 0),
Math.max(bottomRightRadius - borderBottomWidth, 0),
Math.max(bottomLeftRadius - borderLeftWidth, 0),
Math.max(bottomLeftRadius - borderBottomWidth, 0),
innerTopLeftRadiusX,
innerTopLeftRadiusY,
innerTopRightRadiusX,
innerTopRightRadiusY,
innerBottomRightRadiusX,
innerBottomRightRadiusY,
innerBottomLeftRadiusX,
innerBottomLeftRadiusY,
},
Path.Direction.CW);
@ -414,6 +488,259 @@ public class ReactViewBackgroundDrawable extends Drawable {
bottomLeftRadius + extraRadiusForOutline
},
Path.Direction.CW);
/**
* Rounded Multi-Colored Border Algorithm:
*
* <p>Let O (for outer) = (top, left, bottom, right) be the rectangle that represents the size
* and position of a view V. Since the box-sizing of all React Native views is border-box, any
* border of V will render inside O.
*
* <p>Let BorderWidth = (borderTop, borderLeft, borderBottom, borderRight).
* <p>Let I (for inner) = O - BorderWidth.
*
* <p>Then, remembering that O and I are rectangles and that I is inside O, O - I gives us the
* border of V. Therefore, we can use canvas.clipPath to draw V's border.
*
* <p>canvas.clipPath(O, Region.OP.INTERSECT);
* <p>canvas.clipPath(I, Region.OP.DIFFERENCE);
* <p>canvas.drawRect(O, paint);
*
* <p>This lets us draw non-rounded single-color borders.
*
* <p>To extend this algorithm to rounded single-color borders, we:
* <p>1. Curve the corners of O by the (border radii of V) using Path#addRoundRect.
* <p>2. Curve the corners of I by (border radii of V - border widths of V) using
* Path#addRoundRect.
*
* <p>Let O' = curve(O, border radii of V).
* <p>Let I' = curve(I, border radii of V - border widths of V)
*
* <p>The rationale behind this decision is the (first sentence of the) following section in the
* CSS Backgrounds and Borders Module Level 3:
* https://www.w3.org/TR/css3-background/#the-border-radius.
*
* <p>After both O and I have been curved, we can execute the following lines once again to
* render curved single-color borders:
*
* <p>canvas.clipPath(O, Region.OP.INTERSECT);
* <p>canvas.clipPath(I, Region.OP.DIFFERENCE);
* <p>canvas.drawRect(O, paint);
*
* <p>To extend this algorithm to rendering multi-colored rounded borders, we render each side
* of the border as its own quadrilateral. Suppose that we were handling the case where all the
* border radii are 0. Then, the four quadrilaterals would be:
*
* <p>Left: (O.left, O.top), (I.left, I.top), (I.left, I.bottom), (O.left, O.bottom)
* <p>Top: (O.left, O.top), (I.left, I.top), (I.right, I.top), (O.right, O.top)
* <p>Right: (O.right, O.top), (I.right, I.top), (I.right, I.bottom), (O.right, O.bottom)
* <p>Bottom: (O.right, O.bottom), (I.right, I.bottom), (I.left, I.bottom), (O.left, O.bottom)
*
* <p>Now, lets consider what happens when we render a rounded border (radii != 0). For the sake
* of simplicity, let's focus on the top edge of the Left border:
*
* <p>Let borderTopLeftRadius = 5. Let borderLeftWidth = 1. Let borderTopWidth = 2.
*
* <p>We know that O is curved by the ellipse E_O (a = 5, b = 5). We know that I is curved by
* the ellipse E_I (a = 5 - 1, b = 5 - 2).
*
* <p>Since we have clipping, it should be safe to set the top-left point of the Left
* quadrilateral's top edge to (O.left, O.top).
*
* <p>But, what should the top-right point be?
*
* <p>The fact that the border is curved shouldn't change the slope (nor the position) of the
* line connecting the top-left and top-right points of the Left quadrilateral's top edge.
* Therefore, The top-right point should lie somewhere on the line L = (1 - a) * (O.left, O.top)
* + a * (I.left, I.top).
*
* <p>a != 0, because then the top-left and top-right points would be the same and
* borderLeftWidth = 1. a != 1, because then the top-right point would not touch an edge of the
* ellipse E_I. We want the top-right point to touch an edge of the inner ellipse because the
* border curves with E_I on the top-left corner of V.
*
* <p>Therefore, it must be the case that a > 1. Two natural locations of the top-right point
* exist: 1. The first intersection of L with E_I. 2. The second intersection of L with E_I.
*
* <p>We choose the top-right point of the top edge of the Left quadrilateral to be an arbitrary
* intersection of L with E_I.
*/
if (mInnerTopLeftCorner == null) {
mInnerTopLeftCorner = new PointF();
}
/** Compute mInnerTopLeftCorner */
mInnerTopLeftCorner.x = mInnerClipTempRectForBorderRadius.left;
mInnerTopLeftCorner.y = mInnerClipTempRectForBorderRadius.top;
getEllipseIntersectionWithLine(
// Ellipse Bounds
mInnerClipTempRectForBorderRadius.left,
mInnerClipTempRectForBorderRadius.top,
mInnerClipTempRectForBorderRadius.left + 2 * innerTopLeftRadiusX,
mInnerClipTempRectForBorderRadius.top + 2 * innerTopLeftRadiusY,
// Line Start
mOuterClipTempRectForBorderRadius.left,
mOuterClipTempRectForBorderRadius.top,
// Line End
mInnerClipTempRectForBorderRadius.left,
mInnerClipTempRectForBorderRadius.top,
// Result
mInnerTopLeftCorner);
/** Compute mInnerBottomLeftCorner */
if (mInnerBottomLeftCorner == null) {
mInnerBottomLeftCorner = new PointF();
}
mInnerBottomLeftCorner.x = mInnerClipTempRectForBorderRadius.left;
mInnerBottomLeftCorner.y = mInnerClipTempRectForBorderRadius.bottom;
getEllipseIntersectionWithLine(
// Ellipse Bounds
mInnerClipTempRectForBorderRadius.left,
mInnerClipTempRectForBorderRadius.bottom - 2 * innerBottomLeftRadiusY,
mInnerClipTempRectForBorderRadius.left + 2 * innerBottomLeftRadiusX,
mInnerClipTempRectForBorderRadius.bottom,
// Line Start
mOuterClipTempRectForBorderRadius.left,
mOuterClipTempRectForBorderRadius.bottom,
// Line End
mInnerClipTempRectForBorderRadius.left,
mInnerClipTempRectForBorderRadius.bottom,
// Result
mInnerBottomLeftCorner);
/** Compute mInnerTopRightCorner */
if (mInnerTopRightCorner == null) {
mInnerTopRightCorner = new PointF();
}
mInnerTopRightCorner.x = mInnerClipTempRectForBorderRadius.right;
mInnerTopRightCorner.y = mInnerClipTempRectForBorderRadius.top;
getEllipseIntersectionWithLine(
// Ellipse Bounds
mInnerClipTempRectForBorderRadius.right - 2 * innerTopRightRadiusX,
mInnerClipTempRectForBorderRadius.top,
mInnerClipTempRectForBorderRadius.right,
mInnerClipTempRectForBorderRadius.top + 2 * innerTopRightRadiusY,
// Line Start
mOuterClipTempRectForBorderRadius.right,
mOuterClipTempRectForBorderRadius.top,
// Line End
mInnerClipTempRectForBorderRadius.right,
mInnerClipTempRectForBorderRadius.top,
// Result
mInnerTopRightCorner);
/** Compute mInnerBottomRightCorner */
if (mInnerBottomRightCorner == null) {
mInnerBottomRightCorner = new PointF();
}
mInnerBottomRightCorner.x = mInnerClipTempRectForBorderRadius.right;
mInnerBottomRightCorner.y = mInnerClipTempRectForBorderRadius.bottom;
getEllipseIntersectionWithLine(
// Ellipse Bounds
mInnerClipTempRectForBorderRadius.right - 2 * innerBottomRightRadiusX,
mInnerClipTempRectForBorderRadius.bottom - 2 * innerBottomRightRadiusY,
mInnerClipTempRectForBorderRadius.right,
mInnerClipTempRectForBorderRadius.bottom,
// Line Start
mOuterClipTempRectForBorderRadius.right,
mOuterClipTempRectForBorderRadius.bottom,
// Line End
mInnerClipTempRectForBorderRadius.right,
mInnerClipTempRectForBorderRadius.bottom,
// Result
mInnerBottomRightCorner);
}
private static void getEllipseIntersectionWithLine(
double ellipseBoundsLeft,
double ellipseBoundsTop,
double ellipseBoundsRight,
double ellipseBoundsBottom,
double lineStartX,
double lineStartY,
double lineEndX,
double lineEndY,
PointF result) {
final double ellipseCenterX = (ellipseBoundsLeft + ellipseBoundsRight) / 2;
final double ellipseCenterY = (ellipseBoundsTop + ellipseBoundsBottom) / 2;
/**
* Step 1:
*
* Translate the line so that the ellipse is at the origin.
*
* Why? It makes the math easier by changing the ellipse equation from
* ((x - ellipseCenterX)/a)^2 + ((y - ellipseCenterY)/b)^2 = 1 to
* (x/a)^2 + (y/b)^2 = 1.
*/
lineStartX -= ellipseCenterX;
lineStartY -= ellipseCenterY;
lineEndX -= ellipseCenterX;
lineEndY -= ellipseCenterY;
/**
* Step 2:
*
* Ellipse equation: (x/a)^2 + (y/b)^2 = 1
* Line equation: y = mx + c
*/
final double a = Math.abs(ellipseBoundsRight - ellipseBoundsLeft) / 2;
final double b = Math.abs(ellipseBoundsBottom - ellipseBoundsTop) / 2;
final double m = (lineEndY - lineStartY) / (lineEndX - lineStartX);
final double c = lineStartY - m * lineStartX; // Just a point on the line
/**
* Step 3:
*
* Substitute the Line equation into the Ellipse equation. Solve for x.
* Eventually, you'll have to use the quadratic formula.
*
* Quadratic formula: Ax^2 + Bx + C = 0
*/
final double A = (b * b + a * a * m * m);
final double B = 2 * a * a * c * m;
final double C = (a * a * (c * c - b * b));
/**
* Step 4:
*
* Apply Quadratic formula. D = determinant / 2A
*/
final double D = Math.sqrt(-C / A + Math.pow(B / (2 * A), 2));
final double x2 = -B / (2 * A) - D;
final double y2 = m * x2 + c;
/**
* Step 5:
*
* Undo the space transformation in Step 5.
*/
final double x = x2 + ellipseCenterX;
final double y = y2 + ellipseCenterY;
if (!Double.isNaN(x) && !Double.isNaN(y)) {
result.x = (float) x;
result.y = (float) y;
}
}
public float getBorderWidthOrDefaultTo(final float defaultValue, final int spacingType) {
@ -447,18 +774,6 @@ public class ReactViewBackgroundDrawable extends Drawable {
mBorderWidth.getRaw(Spacing.ALL) : 0f;
}
/**
* We use this method for getting color for rounded borders only similarly as for
* {@link #getFullBorderWidth}.
*/
private int getFullBorderColor() {
float rgb = (mBorderRGB != null && !YogaConstants.isUndefined(mBorderRGB.getRaw(Spacing.ALL))) ?
mBorderRGB.getRaw(Spacing.ALL) : DEFAULT_BORDER_RGB;
float alpha = (mBorderAlpha != null && !YogaConstants.isUndefined(mBorderAlpha.getRaw(Spacing.ALL))) ?
mBorderAlpha.getRaw(Spacing.ALL) : DEFAULT_BORDER_ALPHA;
return ReactViewBackgroundDrawable.colorFromAlphaAndRGBComponents(alpha, rgb);
}
/**
* Quickly determine if all the set border colors are equal. Bitwise AND all the set colors
* together, then OR them all together. If the AND and the OR are the same, then the colors
@ -547,10 +862,6 @@ public class ReactViewBackgroundDrawable extends Drawable {
}
}
} else {
if (mPathForBorder == null) {
mPathForBorder = new Path();
}
// If the path drawn previously is of the same color,
// there would be a slight white space between borders
// with anti-alias set to true.
@ -562,48 +873,56 @@ public class ReactViewBackgroundDrawable extends Drawable {
int width = bounds.width();
int height = bounds.height();
if (borderLeft > 0 && colorLeft != Color.TRANSPARENT) {
mPaint.setColor(colorLeft);
mPathForBorder.reset();
mPathForBorder.moveTo(left, top);
mPathForBorder.lineTo(left + borderLeft, top + borderTop);
mPathForBorder.lineTo(left + borderLeft, top + height - borderBottom);
mPathForBorder.lineTo(left, top + height);
mPathForBorder.lineTo(left, top);
canvas.drawPath(mPathForBorder, mPaint);
if (borderLeft > 0) {
final float x1 = left;
final float y1 = top;
final float x2 = left + borderLeft;
final float y2 = top + borderTop;
final float x3 = left + borderLeft;
final float y3 = top + height - borderBottom;
final float x4 = left;
final float y4 = top + height;
drawQuadrilateral(canvas, colorLeft, x1, y1, x2, y2, x3, y3, x4, y4);
}
if (borderTop > 0 && colorTop != Color.TRANSPARENT) {
mPaint.setColor(colorTop);
mPathForBorder.reset();
mPathForBorder.moveTo(left, top);
mPathForBorder.lineTo(left + borderLeft, top + borderTop);
mPathForBorder.lineTo(left + width - borderRight, top + borderTop);
mPathForBorder.lineTo(left + width, top);
mPathForBorder.lineTo(left, top);
canvas.drawPath(mPathForBorder, mPaint);
if (borderTop > 0) {
final float x1 = left;
final float y1 = top;
final float x2 = left + borderLeft;
final float y2 = top + borderTop;
final float x3 = left + width - borderRight;
final float y3 = top + borderTop;
final float x4 = left + width;
final float y4 = top;
drawQuadrilateral(canvas, colorTop, x1, y1, x2, y2, x3, y3, x4, y4);
}
if (borderRight > 0 && colorRight != Color.TRANSPARENT) {
mPaint.setColor(colorRight);
mPathForBorder.reset();
mPathForBorder.moveTo(left + width, top);
mPathForBorder.lineTo(left + width, top + height);
mPathForBorder.lineTo(left + width - borderRight, top + height - borderBottom);
mPathForBorder.lineTo(left + width - borderRight, top + borderTop);
mPathForBorder.lineTo(left + width, top);
canvas.drawPath(mPathForBorder, mPaint);
if (borderRight > 0) {
final float x1 = left + width;
final float y1 = top;
final float x2 = left + width;
final float y2 = top + height;
final float x3 = left + width - borderRight;
final float y3 = top + height - borderBottom;
final float x4 = left + width - borderRight;
final float y4 = top + borderTop;
drawQuadrilateral(canvas, colorRight, x1, y1, x2, y2, x3, y3, x4, y4);
}
if (borderBottom > 0 && colorBottom != Color.TRANSPARENT) {
mPaint.setColor(colorBottom);
mPathForBorder.reset();
mPathForBorder.moveTo(left, top + height);
mPathForBorder.lineTo(left + width, top + height);
mPathForBorder.lineTo(left + width - borderRight, top + height - borderBottom);
mPathForBorder.lineTo(left + borderLeft, top + height - borderBottom);
mPathForBorder.lineTo(left, top + height);
canvas.drawPath(mPathForBorder, mPaint);
if (borderBottom > 0) {
final float x1 = left;
final float y1 = top + height;
final float x2 = left + width;
final float y2 = top + height;
final float x3 = left + width - borderRight;
final float y3 = top + height - borderBottom;
final float x4 = left + borderLeft;
final float y4 = top + height - borderBottom;
drawQuadrilateral(canvas, colorBottom, x1, y1, x2, y2, x3, y3, x4, y4);
}
// re-enable anti alias
@ -612,6 +931,35 @@ public class ReactViewBackgroundDrawable extends Drawable {
}
}
private void drawQuadrilateral(
Canvas canvas,
int fillColor,
float x1,
float y1,
float x2,
float y2,
float x3,
float y3,
float x4,
float y4) {
if (fillColor == Color.TRANSPARENT) {
return;
}
if (mPathForBorder == null) {
mPathForBorder = new Path();
}
mPaint.setColor(fillColor);
mPathForBorder.reset();
mPathForBorder.moveTo(x1, y1);
mPathForBorder.lineTo(x2, y2);
mPathForBorder.lineTo(x3, y3);
mPathForBorder.lineTo(x4, y4);
mPathForBorder.lineTo(x1, y1);
canvas.drawPath(mPathForBorder, mPaint);
}
private int getBorderWidth(int position) {
return mBorderWidth != null ? Math.round(mBorderWidth.get(position)) : 0;
}