543 lines
20 KiB
Mathematica
543 lines
20 KiB
Mathematica
|
/*
|
||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||
|
*
|
||
|
* This source code is licensed under the MIT license found in the
|
||
|
* LICENSE file in the root directory of this source tree.
|
||
|
*/
|
||
|
|
||
|
#import "RCTBorderDrawing.h"
|
||
|
#import "RCTLog.h"
|
||
|
|
||
|
static const CGFloat RCTViewBorderThreshold = 0.001;
|
||
|
|
||
|
BOOL RCTBorderInsetsAreEqual(UIEdgeInsets borderInsets)
|
||
|
{
|
||
|
return ABS(borderInsets.left - borderInsets.right) < RCTViewBorderThreshold &&
|
||
|
ABS(borderInsets.left - borderInsets.bottom) < RCTViewBorderThreshold &&
|
||
|
ABS(borderInsets.left - borderInsets.top) < RCTViewBorderThreshold;
|
||
|
}
|
||
|
|
||
|
BOOL RCTCornerRadiiAreEqual(RCTCornerRadii cornerRadii)
|
||
|
{
|
||
|
return ABS(cornerRadii.topLeft - cornerRadii.topRight) < RCTViewBorderThreshold &&
|
||
|
ABS(cornerRadii.topLeft - cornerRadii.bottomLeft) < RCTViewBorderThreshold &&
|
||
|
ABS(cornerRadii.topLeft - cornerRadii.bottomRight) < RCTViewBorderThreshold;
|
||
|
}
|
||
|
|
||
|
BOOL RCTBorderColorsAreEqual(RCTBorderColors borderColors)
|
||
|
{
|
||
|
return CGColorEqualToColor(borderColors.left, borderColors.right) &&
|
||
|
CGColorEqualToColor(borderColors.left, borderColors.top) &&
|
||
|
CGColorEqualToColor(borderColors.left, borderColors.bottom);
|
||
|
}
|
||
|
|
||
|
RCTCornerInsets RCTGetCornerInsets(RCTCornerRadii cornerRadii, UIEdgeInsets edgeInsets)
|
||
|
{
|
||
|
return (RCTCornerInsets){
|
||
|
{
|
||
|
MAX(0, cornerRadii.topLeft - edgeInsets.left),
|
||
|
MAX(0, cornerRadii.topLeft - edgeInsets.top),
|
||
|
},
|
||
|
{
|
||
|
MAX(0, cornerRadii.topRight - edgeInsets.right),
|
||
|
MAX(0, cornerRadii.topRight - edgeInsets.top),
|
||
|
},
|
||
|
{
|
||
|
MAX(0, cornerRadii.bottomLeft - edgeInsets.left),
|
||
|
MAX(0, cornerRadii.bottomLeft - edgeInsets.bottom),
|
||
|
},
|
||
|
{
|
||
|
MAX(0, cornerRadii.bottomRight - edgeInsets.right),
|
||
|
MAX(0, cornerRadii.bottomRight - edgeInsets.bottom),
|
||
|
}};
|
||
|
}
|
||
|
|
||
|
static UIEdgeInsets RCTRoundInsetsToPixel(UIEdgeInsets edgeInsets)
|
||
|
{
|
||
|
edgeInsets.top = RCTRoundPixelValue(edgeInsets.top);
|
||
|
edgeInsets.bottom = RCTRoundPixelValue(edgeInsets.bottom);
|
||
|
edgeInsets.left = RCTRoundPixelValue(edgeInsets.left);
|
||
|
edgeInsets.right = RCTRoundPixelValue(edgeInsets.right);
|
||
|
|
||
|
return edgeInsets;
|
||
|
}
|
||
|
|
||
|
static void RCTPathAddEllipticArc(
|
||
|
CGMutablePathRef path,
|
||
|
const CGAffineTransform *m,
|
||
|
CGPoint origin,
|
||
|
CGSize size,
|
||
|
CGFloat startAngle,
|
||
|
CGFloat endAngle,
|
||
|
BOOL clockwise)
|
||
|
{
|
||
|
CGFloat xScale = 1, yScale = 1, radius = 0;
|
||
|
if (size.width != 0) {
|
||
|
xScale = 1;
|
||
|
yScale = size.height / size.width;
|
||
|
radius = size.width;
|
||
|
} else if (size.height != 0) {
|
||
|
xScale = size.width / size.height;
|
||
|
yScale = 1;
|
||
|
radius = size.height;
|
||
|
}
|
||
|
|
||
|
CGAffineTransform t = CGAffineTransformMakeTranslation(origin.x, origin.y);
|
||
|
t = CGAffineTransformScale(t, xScale, yScale);
|
||
|
if (m != NULL) {
|
||
|
t = CGAffineTransformConcat(t, *m);
|
||
|
}
|
||
|
|
||
|
CGPathAddArc(path, &t, 0, 0, radius, startAngle, endAngle, clockwise);
|
||
|
}
|
||
|
|
||
|
CGPathRef RCTPathCreateWithRoundedRect(CGRect bounds, RCTCornerInsets cornerInsets, const CGAffineTransform *transform)
|
||
|
{
|
||
|
const CGFloat minX = CGRectGetMinX(bounds);
|
||
|
const CGFloat minY = CGRectGetMinY(bounds);
|
||
|
const CGFloat maxX = CGRectGetMaxX(bounds);
|
||
|
const CGFloat maxY = CGRectGetMaxY(bounds);
|
||
|
|
||
|
const CGSize topLeft = {
|
||
|
MAX(0, MIN(cornerInsets.topLeft.width, bounds.size.width - cornerInsets.topRight.width)),
|
||
|
MAX(0, MIN(cornerInsets.topLeft.height, bounds.size.height - cornerInsets.bottomLeft.height)),
|
||
|
};
|
||
|
const CGSize topRight = {
|
||
|
MAX(0, MIN(cornerInsets.topRight.width, bounds.size.width - cornerInsets.topLeft.width)),
|
||
|
MAX(0, MIN(cornerInsets.topRight.height, bounds.size.height - cornerInsets.bottomRight.height)),
|
||
|
};
|
||
|
const CGSize bottomLeft = {
|
||
|
MAX(0, MIN(cornerInsets.bottomLeft.width, bounds.size.width - cornerInsets.bottomRight.width)),
|
||
|
MAX(0, MIN(cornerInsets.bottomLeft.height, bounds.size.height - cornerInsets.topLeft.height)),
|
||
|
};
|
||
|
const CGSize bottomRight = {
|
||
|
MAX(0, MIN(cornerInsets.bottomRight.width, bounds.size.width - cornerInsets.bottomLeft.width)),
|
||
|
MAX(0, MIN(cornerInsets.bottomRight.height, bounds.size.height - cornerInsets.topRight.height)),
|
||
|
};
|
||
|
|
||
|
CGMutablePathRef path = CGPathCreateMutable();
|
||
|
RCTPathAddEllipticArc(
|
||
|
path, transform, (CGPoint){minX + topLeft.width, minY + topLeft.height}, topLeft, M_PI, 3 * M_PI_2, NO);
|
||
|
RCTPathAddEllipticArc(
|
||
|
path, transform, (CGPoint){maxX - topRight.width, minY + topRight.height}, topRight, 3 * M_PI_2, 0, NO);
|
||
|
RCTPathAddEllipticArc(
|
||
|
path, transform, (CGPoint){maxX - bottomRight.width, maxY - bottomRight.height}, bottomRight, 0, M_PI_2, NO);
|
||
|
RCTPathAddEllipticArc(
|
||
|
path, transform, (CGPoint){minX + bottomLeft.width, maxY - bottomLeft.height}, bottomLeft, M_PI_2, M_PI, NO);
|
||
|
CGPathCloseSubpath(path);
|
||
|
return path;
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
RCTEllipseGetIntersectionsWithLine(CGRect ellipseBounds, CGPoint lineStart, CGPoint lineEnd, CGPoint intersections[2])
|
||
|
{
|
||
|
const CGPoint ellipseCenter = {CGRectGetMidX(ellipseBounds), CGRectGetMidY(ellipseBounds)};
|
||
|
|
||
|
lineStart.x -= ellipseCenter.x;
|
||
|
lineStart.y -= ellipseCenter.y;
|
||
|
lineEnd.x -= ellipseCenter.x;
|
||
|
lineEnd.y -= ellipseCenter.y;
|
||
|
|
||
|
const CGFloat m = (lineEnd.y - lineStart.y) / (lineEnd.x - lineStart.x);
|
||
|
const CGFloat a = ellipseBounds.size.width / 2;
|
||
|
const CGFloat b = ellipseBounds.size.height / 2;
|
||
|
const CGFloat c = lineStart.y - m * lineStart.x;
|
||
|
const CGFloat A = (b * b + a * a * m * m);
|
||
|
const CGFloat B = 2 * a * a * c * m;
|
||
|
const CGFloat D = sqrt((a * a * (b * b - c * c)) / A + pow(B / (2 * A), 2));
|
||
|
|
||
|
const CGFloat x_ = -B / (2 * A);
|
||
|
const CGFloat x1 = x_ + D;
|
||
|
const CGFloat x2 = x_ - D;
|
||
|
const CGFloat y1 = m * x1 + c;
|
||
|
const CGFloat y2 = m * x2 + c;
|
||
|
|
||
|
intersections[0] = (CGPoint){x1 + ellipseCenter.x, y1 + ellipseCenter.y};
|
||
|
intersections[1] = (CGPoint){x2 + ellipseCenter.x, y2 + ellipseCenter.y};
|
||
|
}
|
||
|
|
||
|
NS_INLINE BOOL RCTCornerRadiiAreAboveThreshold(RCTCornerRadii cornerRadii)
|
||
|
{
|
||
|
return (
|
||
|
cornerRadii.topLeft > RCTViewBorderThreshold || cornerRadii.topRight > RCTViewBorderThreshold ||
|
||
|
cornerRadii.bottomLeft > RCTViewBorderThreshold || cornerRadii.bottomRight > RCTViewBorderThreshold);
|
||
|
}
|
||
|
|
||
|
static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCornerRadii cornerRadii)
|
||
|
{
|
||
|
if (drawToEdge) {
|
||
|
return CGPathCreateWithRect(rect, NULL);
|
||
|
}
|
||
|
|
||
|
return RCTPathCreateWithRoundedRect(rect, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL);
|
||
|
}
|
||
|
|
||
|
static UIGraphicsImageRenderer *
|
||
|
RCTUIGraphicsImageRenderer(CGSize size, CGColorRef backgroundColor, BOOL hasCornerRadii, BOOL drawToEdge)
|
||
|
{
|
||
|
const CGFloat alpha = CGColorGetAlpha(backgroundColor);
|
||
|
const BOOL opaque = (drawToEdge || !hasCornerRadii) && alpha == 1.0;
|
||
|
UIGraphicsImageRendererFormat *const rendererFormat = [UIGraphicsImageRendererFormat defaultFormat];
|
||
|
rendererFormat.opaque = opaque;
|
||
|
UIGraphicsImageRenderer *const renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size format:rendererFormat];
|
||
|
return renderer;
|
||
|
}
|
||
|
|
||
|
static UIImage *RCTGetSolidBorderImage(
|
||
|
RCTCornerRadii cornerRadii,
|
||
|
CGSize viewSize,
|
||
|
UIEdgeInsets borderInsets,
|
||
|
RCTBorderColors borderColors,
|
||
|
CGColorRef backgroundColor,
|
||
|
BOOL drawToEdge)
|
||
|
{
|
||
|
const BOOL hasCornerRadii = RCTCornerRadiiAreAboveThreshold(cornerRadii);
|
||
|
const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, borderInsets);
|
||
|
|
||
|
// Incorrect render for borders that are not proportional to device pixel: borders get stretched and become
|
||
|
// significantly bigger than expected.
|
||
|
// Rdar: http://www.openradar.me/15959788
|
||
|
borderInsets = RCTRoundInsetsToPixel(borderInsets);
|
||
|
|
||
|
const BOOL makeStretchable =
|
||
|
(borderInsets.left + cornerInsets.topLeft.width + borderInsets.right + cornerInsets.bottomRight.width <=
|
||
|
viewSize.width) &&
|
||
|
(borderInsets.left + cornerInsets.bottomLeft.width + borderInsets.right + cornerInsets.topRight.width <=
|
||
|
viewSize.width) &&
|
||
|
(borderInsets.top + cornerInsets.topLeft.height + borderInsets.bottom + cornerInsets.bottomRight.height <=
|
||
|
viewSize.height) &&
|
||
|
(borderInsets.top + cornerInsets.topRight.height + borderInsets.bottom + cornerInsets.bottomLeft.height <=
|
||
|
viewSize.height);
|
||
|
|
||
|
UIEdgeInsets edgeInsets = (UIEdgeInsets){
|
||
|
borderInsets.top + MAX(cornerInsets.topLeft.height, cornerInsets.topRight.height),
|
||
|
borderInsets.left + MAX(cornerInsets.topLeft.width, cornerInsets.bottomLeft.width),
|
||
|
borderInsets.bottom + MAX(cornerInsets.bottomLeft.height, cornerInsets.bottomRight.height),
|
||
|
borderInsets.right + MAX(cornerInsets.bottomRight.width, cornerInsets.topRight.width)};
|
||
|
|
||
|
if (hasCornerRadii) {
|
||
|
// Asymmetrical edgeInsets cause strange artifacting on iOS 10 and earlier.
|
||
|
edgeInsets = (UIEdgeInsets){
|
||
|
MAX(edgeInsets.top, edgeInsets.bottom),
|
||
|
MAX(edgeInsets.left, edgeInsets.right),
|
||
|
MAX(edgeInsets.top, edgeInsets.bottom),
|
||
|
MAX(edgeInsets.left, edgeInsets.right),
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const CGSize size = makeStretchable ? (CGSize){
|
||
|
// 1pt for the middle stretchable area along each axis
|
||
|
edgeInsets.left + 1 + edgeInsets.right,
|
||
|
edgeInsets.top + 1 + edgeInsets.bottom
|
||
|
} : viewSize;
|
||
|
|
||
|
UIGraphicsImageRenderer *const imageRenderer =
|
||
|
RCTUIGraphicsImageRenderer(size, backgroundColor, hasCornerRadii, drawToEdge);
|
||
|
UIImage *image = [imageRenderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) {
|
||
|
const CGContextRef context = rendererContext.CGContext;
|
||
|
const CGRect rect = {.size = size};
|
||
|
CGPathRef path = RCTPathCreateOuterOutline(drawToEdge, rect, cornerRadii);
|
||
|
|
||
|
if (backgroundColor) {
|
||
|
CGContextSetFillColorWithColor(context, backgroundColor);
|
||
|
CGContextAddPath(context, path);
|
||
|
CGContextFillPath(context);
|
||
|
}
|
||
|
|
||
|
CGContextAddPath(context, path);
|
||
|
CGPathRelease(path);
|
||
|
|
||
|
CGPathRef insetPath = RCTPathCreateWithRoundedRect(UIEdgeInsetsInsetRect(rect, borderInsets), cornerInsets, NULL);
|
||
|
|
||
|
CGContextAddPath(context, insetPath);
|
||
|
CGContextEOClip(context);
|
||
|
|
||
|
BOOL hasEqualColors = RCTBorderColorsAreEqual(borderColors);
|
||
|
if ((drawToEdge || !hasCornerRadii) && hasEqualColors) {
|
||
|
CGContextSetFillColorWithColor(context, borderColors.left);
|
||
|
CGContextAddRect(context, rect);
|
||
|
CGContextAddPath(context, insetPath);
|
||
|
CGContextEOFillPath(context);
|
||
|
|
||
|
} else {
|
||
|
CGPoint topLeft = (CGPoint){borderInsets.left, borderInsets.top};
|
||
|
if (cornerInsets.topLeft.width > 0 && cornerInsets.topLeft.height > 0) {
|
||
|
CGPoint points[2];
|
||
|
RCTEllipseGetIntersectionsWithLine(
|
||
|
(CGRect){topLeft, {2 * cornerInsets.topLeft.width, 2 * cornerInsets.topLeft.height}},
|
||
|
CGPointZero,
|
||
|
topLeft,
|
||
|
points);
|
||
|
if (!isnan(points[1].x) && !isnan(points[1].y)) {
|
||
|
topLeft = points[1];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
CGPoint bottomLeft = (CGPoint){borderInsets.left, size.height - borderInsets.bottom};
|
||
|
if (cornerInsets.bottomLeft.width > 0 && cornerInsets.bottomLeft.height > 0) {
|
||
|
CGPoint points[2];
|
||
|
RCTEllipseGetIntersectionsWithLine(
|
||
|
(CGRect){
|
||
|
{bottomLeft.x, bottomLeft.y - 2 * cornerInsets.bottomLeft.height},
|
||
|
{2 * cornerInsets.bottomLeft.width, 2 * cornerInsets.bottomLeft.height}},
|
||
|
(CGPoint){0, size.height},
|
||
|
bottomLeft,
|
||
|
points);
|
||
|
if (!isnan(points[1].x) && !isnan(points[1].y)) {
|
||
|
bottomLeft = points[1];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
CGPoint topRight = (CGPoint){size.width - borderInsets.right, borderInsets.top};
|
||
|
if (cornerInsets.topRight.width > 0 && cornerInsets.topRight.height > 0) {
|
||
|
CGPoint points[2];
|
||
|
RCTEllipseGetIntersectionsWithLine(
|
||
|
(CGRect){
|
||
|
{topRight.x - 2 * cornerInsets.topRight.width, topRight.y},
|
||
|
{2 * cornerInsets.topRight.width, 2 * cornerInsets.topRight.height}},
|
||
|
(CGPoint){size.width, 0},
|
||
|
topRight,
|
||
|
points);
|
||
|
if (!isnan(points[0].x) && !isnan(points[0].y)) {
|
||
|
topRight = points[0];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
CGPoint bottomRight = (CGPoint){size.width - borderInsets.right, size.height - borderInsets.bottom};
|
||
|
if (cornerInsets.bottomRight.width > 0 && cornerInsets.bottomRight.height > 0) {
|
||
|
CGPoint points[2];
|
||
|
RCTEllipseGetIntersectionsWithLine(
|
||
|
(CGRect){
|
||
|
{bottomRight.x - 2 * cornerInsets.bottomRight.width,
|
||
|
bottomRight.y - 2 * cornerInsets.bottomRight.height},
|
||
|
{2 * cornerInsets.bottomRight.width, 2 * cornerInsets.bottomRight.height}},
|
||
|
(CGPoint){size.width, size.height},
|
||
|
bottomRight,
|
||
|
points);
|
||
|
if (!isnan(points[0].x) && !isnan(points[0].y)) {
|
||
|
bottomRight = points[0];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
CGColorRef currentColor = NULL;
|
||
|
|
||
|
// RIGHT
|
||
|
if (borderInsets.right > 0) {
|
||
|
const CGPoint points[] = {
|
||
|
(CGPoint){size.width, 0},
|
||
|
topRight,
|
||
|
bottomRight,
|
||
|
(CGPoint){size.width, size.height},
|
||
|
};
|
||
|
|
||
|
currentColor = borderColors.right;
|
||
|
CGContextAddLines(context, points, sizeof(points) / sizeof(*points));
|
||
|
}
|
||
|
|
||
|
// BOTTOM
|
||
|
if (borderInsets.bottom > 0) {
|
||
|
const CGPoint points[] = {
|
||
|
(CGPoint){0, size.height},
|
||
|
bottomLeft,
|
||
|
bottomRight,
|
||
|
(CGPoint){size.width, size.height},
|
||
|
};
|
||
|
|
||
|
if (!CGColorEqualToColor(currentColor, borderColors.bottom)) {
|
||
|
CGContextSetFillColorWithColor(context, currentColor);
|
||
|
CGContextFillPath(context);
|
||
|
currentColor = borderColors.bottom;
|
||
|
}
|
||
|
CGContextAddLines(context, points, sizeof(points) / sizeof(*points));
|
||
|
}
|
||
|
|
||
|
// LEFT
|
||
|
if (borderInsets.left > 0) {
|
||
|
const CGPoint points[] = {
|
||
|
CGPointZero,
|
||
|
topLeft,
|
||
|
bottomLeft,
|
||
|
(CGPoint){0, size.height},
|
||
|
};
|
||
|
|
||
|
if (!CGColorEqualToColor(currentColor, borderColors.left)) {
|
||
|
CGContextSetFillColorWithColor(context, currentColor);
|
||
|
CGContextFillPath(context);
|
||
|
currentColor = borderColors.left;
|
||
|
}
|
||
|
CGContextAddLines(context, points, sizeof(points) / sizeof(*points));
|
||
|
}
|
||
|
|
||
|
// TOP
|
||
|
if (borderInsets.top > 0) {
|
||
|
const CGPoint points[] = {
|
||
|
CGPointZero,
|
||
|
topLeft,
|
||
|
topRight,
|
||
|
(CGPoint){size.width, 0},
|
||
|
};
|
||
|
|
||
|
if (!CGColorEqualToColor(currentColor, borderColors.top)) {
|
||
|
CGContextSetFillColorWithColor(context, currentColor);
|
||
|
CGContextFillPath(context);
|
||
|
currentColor = borderColors.top;
|
||
|
}
|
||
|
CGContextAddLines(context, points, sizeof(points) / sizeof(*points));
|
||
|
}
|
||
|
|
||
|
CGContextSetFillColorWithColor(context, currentColor);
|
||
|
CGContextFillPath(context);
|
||
|
}
|
||
|
|
||
|
CGPathRelease(insetPath);
|
||
|
}];
|
||
|
|
||
|
if (makeStretchable) {
|
||
|
image = [image resizableImageWithCapInsets:edgeInsets];
|
||
|
}
|
||
|
|
||
|
return image;
|
||
|
}
|
||
|
|
||
|
// Currently, the dashed / dotted implementation only supports a single colour +
|
||
|
// single width, as that's currently required and supported on Android.
|
||
|
//
|
||
|
// Supporting individual widths + colours on each side is possible by modifying
|
||
|
// the current implementation. The idea is that we will draw four different lines
|
||
|
// and clip appropriately for each side (might require adjustment of phase so that
|
||
|
// they line up but even browsers don't do a good job at that).
|
||
|
//
|
||
|
// Firstly, create two paths for the outer and inner paths. The inner path is
|
||
|
// generated exactly the same way as the outer, just given an inset rect, derived
|
||
|
// from the insets on each side. Then clip using the odd-even rule
|
||
|
// (CGContextEOClip()). This will give us a nice rounded (possibly) clip mask.
|
||
|
//
|
||
|
// +----------------------------------+
|
||
|
// |@@@@@@@@ Clipped Space @@@@@@@@@|
|
||
|
// |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
|
||
|
// |@@+----------------------+@@@@@@@@|
|
||
|
// |@@| |@@@@@@@@|
|
||
|
// |@@| |@@@@@@@@|
|
||
|
// |@@| |@@@@@@@@|
|
||
|
// |@@+----------------------+@@@@@@@@|
|
||
|
// |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
|
||
|
// +----------------------------------+
|
||
|
//
|
||
|
// Afterwards, we create a clip path for each border side (CGContextSaveGState()
|
||
|
// and CGContextRestoreGState() when drawing each side). The clip mask for each
|
||
|
// segment is a trapezoid connecting corresponding edges of the inner and outer
|
||
|
// rects. For example, in the case of the top edge, the points would be:
|
||
|
// - (MinX(outer), MinY(outer))
|
||
|
// - (MaxX(outer), MinY(outer))
|
||
|
// - (MinX(inner) + topLeftRadius, MinY(inner) + topLeftRadius)
|
||
|
// - (MaxX(inner) - topRightRadius, MinY(inner) + topRightRadius)
|
||
|
//
|
||
|
// +------------------+
|
||
|
// |\ /|
|
||
|
// | \ / |
|
||
|
// | \ top / |
|
||
|
// | \ / |
|
||
|
// | \ / |
|
||
|
// | +------+ |
|
||
|
// | | | |
|
||
|
// | | | |
|
||
|
// | | | |
|
||
|
// |left | |right|
|
||
|
// | | | |
|
||
|
// | | | |
|
||
|
// | +------+ |
|
||
|
// | / \ |
|
||
|
// | / \ |
|
||
|
// | / \ |
|
||
|
// | / bottom \ |
|
||
|
// |/ \|
|
||
|
// +------------------+
|
||
|
//
|
||
|
//
|
||
|
// Note that this approach will produce discontinuous colour changes at the edge
|
||
|
// (which is okay). The reason is that Quartz does not currently support drawing
|
||
|
// of gradients _along_ a path (NB: clipping a path and drawing a linear gradient
|
||
|
// is _not_ equivalent).
|
||
|
|
||
|
static UIImage *RCTGetDashedOrDottedBorderImage(
|
||
|
RCTBorderStyle borderStyle,
|
||
|
RCTCornerRadii cornerRadii,
|
||
|
CGSize viewSize,
|
||
|
UIEdgeInsets borderInsets,
|
||
|
RCTBorderColors borderColors,
|
||
|
CGColorRef backgroundColor,
|
||
|
BOOL drawToEdge)
|
||
|
{
|
||
|
NSCParameterAssert(borderStyle == RCTBorderStyleDashed || borderStyle == RCTBorderStyleDotted);
|
||
|
|
||
|
if (!RCTBorderColorsAreEqual(borderColors) || !RCTBorderInsetsAreEqual(borderInsets)) {
|
||
|
RCTLogWarn(@"Unsupported dashed / dotted border style");
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
const CGFloat lineWidth = borderInsets.top;
|
||
|
if (lineWidth <= 0.0) {
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
const BOOL hasCornerRadii = RCTCornerRadiiAreAboveThreshold(cornerRadii);
|
||
|
UIGraphicsImageRenderer *const imageRenderer =
|
||
|
RCTUIGraphicsImageRenderer(viewSize, backgroundColor, hasCornerRadii, drawToEdge);
|
||
|
return [imageRenderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) {
|
||
|
const CGContextRef context = rendererContext.CGContext;
|
||
|
const CGRect rect = {.size = viewSize};
|
||
|
|
||
|
if (backgroundColor) {
|
||
|
CGPathRef outerPath = RCTPathCreateOuterOutline(drawToEdge, rect, cornerRadii);
|
||
|
CGContextAddPath(context, outerPath);
|
||
|
CGPathRelease(outerPath);
|
||
|
|
||
|
CGContextSetFillColorWithColor(context, backgroundColor);
|
||
|
CGContextFillPath(context);
|
||
|
}
|
||
|
|
||
|
// Stroking means that the width is divided in half and grows in both directions
|
||
|
// perpendicular to the path, that's why we inset by half the width, so that it
|
||
|
// reaches the edge of the rect.
|
||
|
CGRect pathRect = CGRectInset(rect, lineWidth / 2.0, lineWidth / 2.0);
|
||
|
CGPathRef path = RCTPathCreateWithRoundedRect(pathRect, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL);
|
||
|
|
||
|
CGFloat dashLengths[2];
|
||
|
dashLengths[0] = dashLengths[1] = (borderStyle == RCTBorderStyleDashed ? 3 : 1) * lineWidth;
|
||
|
|
||
|
CGContextSetLineWidth(context, lineWidth);
|
||
|
CGContextSetLineDash(context, 0, dashLengths, sizeof(dashLengths) / sizeof(*dashLengths));
|
||
|
|
||
|
CGContextSetStrokeColorWithColor(context, [UIColor yellowColor].CGColor);
|
||
|
|
||
|
CGContextAddPath(context, path);
|
||
|
CGContextSetStrokeColorWithColor(context, borderColors.top);
|
||
|
CGContextStrokePath(context);
|
||
|
|
||
|
CGPathRelease(path);
|
||
|
}];
|
||
|
}
|
||
|
|
||
|
UIImage *RCTGetBorderImage(
|
||
|
RCTBorderStyle borderStyle,
|
||
|
CGSize viewSize,
|
||
|
RCTCornerRadii cornerRadii,
|
||
|
UIEdgeInsets borderInsets,
|
||
|
RCTBorderColors borderColors,
|
||
|
CGColorRef backgroundColor,
|
||
|
BOOL drawToEdge)
|
||
|
{
|
||
|
switch (borderStyle) {
|
||
|
case RCTBorderStyleSolid:
|
||
|
return RCTGetSolidBorderImage(cornerRadii, viewSize, borderInsets, borderColors, backgroundColor, drawToEdge);
|
||
|
case RCTBorderStyleDashed:
|
||
|
case RCTBorderStyleDotted:
|
||
|
return RCTGetDashedOrDottedBorderImage(
|
||
|
borderStyle, cornerRadii, viewSize, borderInsets, borderColors, backgroundColor, drawToEdge);
|
||
|
case RCTBorderStyleUnset:
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return nil;
|
||
|
}
|