From ff49d86aedc3afbed5c109d60d2b6c02230cb5d4 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 28 May 2015 08:52:46 -0700 Subject: [PATCH] Implemented fast path for same borders/radii --- Examples/UIExplorer/BorderExample.js | 8 +- React/React.xcodeproj/project.pbxproj | 6 + React/Views/RCTBorderDrawing.h | 61 ++++ React/Views/RCTBorderDrawing.m | 331 ++++++++++++++++++++ React/Views/RCTView.m | 417 ++++++-------------------- 5 files changed, 490 insertions(+), 333 deletions(-) create mode 100644 React/Views/RCTBorderDrawing.h create mode 100644 React/Views/RCTBorderDrawing.m diff --git a/Examples/UIExplorer/BorderExample.js b/Examples/UIExplorer/BorderExample.js index 7789459f8..a9436108d 100644 --- a/Examples/UIExplorer/BorderExample.js +++ b/Examples/UIExplorer/BorderExample.js @@ -81,11 +81,15 @@ var styles = StyleSheet.create({ borderTopLeftRadius: 100, }, border7: { - borderRadius: 20, + borderWidth: 10, + borderColor: 'rgba(255,0,0,0.5)', + borderRadius: 30, + overflow: 'hidden', }, border7_inner: { backgroundColor: 'blue', - flex: 1, + width: 100, + height: 100 }, }); diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index cf05f4e39..c7309989b 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */; }; 13C156051AB1A2840079392D /* RCTWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156021AB1A2840079392D /* RCTWebView.m */; }; 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156041AB1A2840079392D /* RCTWebViewManager.m */; }; + 13CC8A821B17642100940AE7 /* RCTBorderDrawing.m in Sources */ = {isa = PBXBuildFile; fileRef = 13CC8A811B17642100940AE7 /* RCTBorderDrawing.m */; }; 13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067491A70F434002CDEE1 /* RCTUIManager.m */; }; 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */; }; 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; }; @@ -155,6 +156,8 @@ 13C325261AA63B6A0048765F /* RCTAutoInsetsProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAutoInsetsProtocol.h; sourceTree = ""; }; 13C325271AA63B6A0048765F /* RCTScrollableProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTScrollableProtocol.h; sourceTree = ""; }; 13C325281AA63B6A0048765F /* RCTViewNodeProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewNodeProtocol.h; sourceTree = ""; }; + 13CC8A801B17642100940AE7 /* RCTBorderDrawing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBorderDrawing.h; sourceTree = ""; }; + 13CC8A811B17642100940AE7 /* RCTBorderDrawing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBorderDrawing.m; sourceTree = ""; }; 13E067481A70F434002CDEE1 /* RCTUIManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIManager.h; sourceTree = ""; }; 13E067491A70F434002CDEE1 /* RCTUIManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIManager.m; sourceTree = ""; }; 13E0674B1A70F44B002CDEE1 /* RCTShadowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowView.h; sourceTree = ""; }; @@ -279,6 +282,8 @@ children = ( 13442BF21AA90E0B0037E5B0 /* RCTAnimationType.h */, 13C325261AA63B6A0048765F /* RCTAutoInsetsProtocol.h */, + 13CC8A801B17642100940AE7 /* RCTBorderDrawing.h */, + 13CC8A811B17642100940AE7 /* RCTBorderDrawing.m */, 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */, 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */, 13456E911ADAD2DE009F94A7 /* RCTConvert+CoreLocation.h */, @@ -513,6 +518,7 @@ 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */, 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */, 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */, + 13CC8A821B17642100940AE7 /* RCTBorderDrawing.m in Sources */, 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */, 13AF20451AE707F9005F5298 /* RCTSlider.m in Sources */, 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */, diff --git a/React/Views/RCTBorderDrawing.h b/React/Views/RCTBorderDrawing.h new file mode 100644 index 000000000..b67074337 --- /dev/null +++ b/React/Views/RCTBorderDrawing.h @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +typedef struct { + CGFloat topLeft; + CGFloat topRight; + CGFloat bottomLeft; + CGFloat bottomRight; +} RCTCornerRadii; + +typedef struct { + CGSize topLeft; + CGSize topRight; + CGSize bottomLeft; + CGSize bottomRight; +} RCTCornerInsets; + +typedef struct { + CGColorRef top; + CGColorRef left; + CGColorRef bottom; + CGColorRef right; +} RCTBorderColors; + +/** + * Determine if the border widths, colors and radii are all equal. + */ +BOOL RCTBorderInsetsAreEqual(UIEdgeInsets borderInsets); +BOOL RCTCornerRadiiAreEqual(RCTCornerRadii cornerRadii); +BOOL RCTBorderColorsAreEqual(RCTBorderColors borderColors); + +/** + * Convert RCTCornerRadii to RCTCornerInsets by applying border insets. + */ +RCTCornerInsets RCTGetCornerInsets(RCTCornerRadii cornerRadii, + UIEdgeInsets borderInsets); + +/** + * Create a CGPath representing a rounded rectangle with the specified bounds + * and corner insets. Note that the CGPathRef must be released by the caller. + */ +CGPathRef RCTPathCreateWithRoundedRect(CGRect bounds, + RCTCornerInsets cornerInsets, + const CGAffineTransform *transform); + +/** + * Draw a CSS-compliant border as a scalable image. + */ +UIImage *RCTGetBorderImage(RCTCornerRadii cornerRadii, + UIEdgeInsets borderInsets, + RCTBorderColors borderColors, + CGColorRef backgroundColor, + BOOL drawToEdge); diff --git a/React/Views/RCTBorderDrawing.m b/React/Views/RCTBorderDrawing.m new file mode 100644 index 000000000..8d9e7c0d4 --- /dev/null +++ b/React/Views/RCTBorderDrawing.m @@ -0,0 +1,331 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTBorderDrawing.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 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 = cornerInsets.topLeft; + const CGSize topRight = cornerInsets.topRight; + const CGSize bottomLeft = cornerInsets.bottomLeft; + const CGSize bottomRight = cornerInsets.bottomRight; + + 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}; +} + +UIImage *RCTGetBorderImage(RCTCornerRadii cornerRadii, + UIEdgeInsets borderInsets, + RCTBorderColors borderColors, + CGColorRef backgroundColor, + BOOL drawToEdge) +{ + const BOOL hasCornerRadii = + cornerRadii.topLeft > RCTViewBorderThreshold || + cornerRadii.topRight > RCTViewBorderThreshold || + cornerRadii.bottomLeft > RCTViewBorderThreshold || + cornerRadii.bottomRight > RCTViewBorderThreshold; + + const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, borderInsets); + + const 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) + }; + + const CGSize size = (CGSize){ + edgeInsets.left + 1 + edgeInsets.right, + edgeInsets.top + 1 + edgeInsets.bottom + }; + + const CGFloat alpha = CGColorGetAlpha(backgroundColor); + const BOOL opaque = (drawToEdge || !hasCornerRadii) && alpha == 1.0; + UIGraphicsBeginImageContextWithOptions(size, opaque, 0.0); + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + const CGRect rect = {.size = size}; + + CGPathRef path; + if (drawToEdge) { + path = CGPathCreateWithRect(rect, NULL); + } else { + path = RCTPathCreateWithRoundedRect(rect, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL); + } + + if (backgroundColor) { + CGContextSetFillColorWithColor(ctx, backgroundColor); + CGContextAddPath(ctx, path); + CGContextFillPath(ctx); + } + + CGContextAddPath(ctx, path); + CGPathRelease(path); + + CGPathRef insetPath = RCTPathCreateWithRoundedRect(UIEdgeInsetsInsetRect(rect, borderInsets), cornerInsets, NULL); + + CGContextAddPath(ctx, insetPath); + CGContextEOClip(ctx); + + BOOL hasEqualColors = RCTBorderColorsAreEqual(borderColors); + if ((drawToEdge || !hasCornerRadii) && hasEqualColors) { + + CGContextSetFillColorWithColor(ctx, borderColors.left); + CGContextAddRect(ctx, rect); + CGContextAddPath(ctx, insetPath); + CGContextEOFillPath(ctx); + + } 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]; + } + } + + // RIGHT + if (borderInsets.right > 0) { + + const CGPoint points[] = { + (CGPoint){size.width, 0}, + topRight, + bottomRight, + (CGPoint){size.width, size.height}, + }; + + CGContextSetFillColorWithColor(ctx, borderColors.right); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + } + + // BOTTOM + if (borderInsets.bottom > 0) { + + const CGPoint points[] = { + (CGPoint){0, size.height}, + bottomLeft, + bottomRight, + (CGPoint){size.width, size.height}, + }; + + CGContextSetFillColorWithColor(ctx, borderColors.bottom); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + } + + // LEFT + if (borderInsets.left > 0) { + + const CGPoint points[] = { + CGPointZero, + topLeft, + bottomLeft, + (CGPoint){0, size.height}, + }; + + CGContextSetFillColorWithColor(ctx, borderColors.left); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + } + + // TOP + if (borderInsets.top > 0) { + + const CGPoint points[] = { + CGPointZero, + topLeft, + topRight, + (CGPoint){size.width, 0}, + }; + + CGContextSetFillColorWithColor(ctx, borderColors.top); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + } + } + + CGPathRelease(insetPath); + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return [image resizableImageWithCapInsets:edgeInsets]; +} diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index d97eb5567..2cc03d874 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -10,13 +10,12 @@ #import "RCTView.h" #import "RCTAutoInsetsProtocol.h" +#import "RCTBorderDrawing.h" #import "RCTConvert.h" #import "RCTLog.h" #import "RCTUtils.h" #import "UIView+React.h" -static const CGFloat RCTViewBorderThreshold = 0.001; - static UIView *RCTViewHitTest(UIView *view, CGPoint point, UIEvent *event) { for (UIView *subview in [view.subviews reverseObjectEnumerator]) { @@ -31,10 +30,6 @@ static UIView *RCTViewHitTest(UIView *view, CGPoint point, UIEvent *event) return nil; } -static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2]); -static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform); -static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise); - @implementation UIView (RCTViewUnmounting) - (void)react_remountAllSubviews @@ -443,246 +438,88 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) [self.layer setNeedsDisplay]; } -- (UIImage *)borderImage:(out CGRect *)contentsCenter +- (UIEdgeInsets)bordersAsInsets { - const CGFloat maxRadius = ({ - const CGRect bounds = self.bounds; - MIN(bounds.size.height, bounds.size.width); - }); - - const CGFloat radius = MAX(0, _borderRadius); - const CGFloat topLeftRadius = MIN(_borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius, maxRadius); - const CGFloat topRightRadius = MIN(_borderTopRightRadius >= 0 ? _borderTopRightRadius : radius, maxRadius); - const CGFloat bottomLeftRadius = MIN(_borderBottomLeftRadius >= 0 ? _borderBottomLeftRadius : radius, maxRadius); - const CGFloat bottomRightRadius = MIN(_borderBottomRightRadius >= 0 ? _borderBottomRightRadius : radius, maxRadius); - const CGFloat borderWidth = MAX(0, _borderWidth); - const CGFloat topWidth = _borderTopWidth >= 0 ? _borderTopWidth : borderWidth; - const CGFloat rightWidth = _borderRightWidth >= 0 ? _borderRightWidth : borderWidth; - const CGFloat bottomWidth = _borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth; - const CGFloat leftWidth = _borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth; - const BOOL hasCornerRadii = - topLeftRadius > RCTViewBorderThreshold || - topRightRadius > RCTViewBorderThreshold || - bottomLeftRadius > RCTViewBorderThreshold || - bottomRightRadius > RCTViewBorderThreshold; + return (UIEdgeInsets) { + _borderTopWidth >= 0 ? _borderTopWidth : borderWidth, + _borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth, + _borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth, + _borderRightWidth >= 0 ? _borderRightWidth : borderWidth, + }; +} - const BOOL hasBorders = - topWidth > RCTViewBorderThreshold || - rightWidth > RCTViewBorderThreshold || - bottomWidth > RCTViewBorderThreshold || - leftWidth > RCTViewBorderThreshold; +- (RCTCornerRadii)cornerRadii +{ + const CGRect bounds = self.bounds; + const CGFloat maxRadius = MIN(bounds.size.height, bounds.size.width); + const CGFloat radius = MAX(0, _borderRadius); - if (!hasCornerRadii && !hasBorders) { - return nil; - } + return (RCTCornerRadii){ + MIN(_borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius, maxRadius), + MIN(_borderTopRightRadius >= 0 ? _borderTopRightRadius : radius, maxRadius), + MIN(_borderBottomLeftRadius >= 0 ? _borderBottomLeftRadius : radius, maxRadius), + MIN(_borderBottomRightRadius >= 0 ? _borderBottomRightRadius : radius, maxRadius), + }; +} - const CGFloat innerTopLeftRadiusX = MAX(0, topLeftRadius - leftWidth); - const CGFloat innerTopLeftRadiusY = MAX(0, topLeftRadius - topWidth); - - const CGFloat innerTopRightRadiusX = MAX(0, topRightRadius - rightWidth); - const CGFloat innerTopRightRadiusY = MAX(0, topRightRadius - topWidth); - - const CGFloat innerBottomLeftRadiusX = MAX(0, bottomLeftRadius - leftWidth); - const CGFloat innerBottomLeftRadiusY = MAX(0, bottomLeftRadius - bottomWidth); - - const CGFloat innerBottomRightRadiusX = MAX(0, bottomRightRadius - rightWidth); - const CGFloat innerBottomRightRadiusY = MAX(0, bottomRightRadius - bottomWidth); - - const UIEdgeInsets edgeInsets = UIEdgeInsetsMake(topWidth + MAX(innerTopLeftRadiusY, innerTopRightRadiusY), leftWidth + MAX(innerTopLeftRadiusX, innerBottomLeftRadiusX), bottomWidth + MAX(innerBottomLeftRadiusY, innerBottomRightRadiusY), rightWidth + + MAX(innerBottomRightRadiusX, innerTopRightRadiusX)); - const CGSize size = CGSizeMake(edgeInsets.left + 1 + edgeInsets.right, edgeInsets.top + 1 + edgeInsets.bottom); - - const CGFloat alpha = CGColorGetAlpha(_backgroundColor.CGColor); - const BOOL opaque = (self.clipsToBounds || !hasCornerRadii) && alpha == 1.0; - UIGraphicsBeginImageContextWithOptions(size, opaque, 0.0); - - CGContextRef ctx = UIGraphicsGetCurrentContext(); - const CGRect rect = {.size = size}; - - CGPathRef path; - const BOOL hasClipping = self.clipsToBounds; - if (hasClipping) { - path = CGPathCreateWithRect(rect, NULL); - } else { - path = RCTPathCreateWithRoundedRect(rect, topLeftRadius, topLeftRadius, topRightRadius, topRightRadius, bottomLeftRadius, bottomLeftRadius, bottomRightRadius, bottomRightRadius, NULL); - } - - if (_backgroundColor) { - CGContextSaveGState(ctx); - - CGContextSetFillColorWithColor(ctx, _backgroundColor.CGColor); - CGContextAddPath(ctx, path); - CGContextFillPath(ctx); - - CGContextRestoreGState(ctx); - } - - CGContextAddPath(ctx, path); - CGPathRelease(path); - - const BOOL hasRadius = topLeftRadius > 0 || topRightRadius > 0 || bottomLeftRadius > 0 || bottomRightRadius > 0; - const UIEdgeInsets insetEdgeInsets = UIEdgeInsetsMake(topWidth, leftWidth, bottomWidth, rightWidth); - CGPathRef insetPath = RCTPathCreateWithRoundedRect(UIEdgeInsetsInsetRect(rect, insetEdgeInsets), innerTopLeftRadiusX, innerTopLeftRadiusY, innerTopRightRadiusX, innerTopRightRadiusY, innerBottomLeftRadiusX, innerBottomLeftRadiusY, innerBottomRightRadiusX, innerBottomRightRadiusY, NULL); - - CGContextAddPath(ctx, insetPath); - CGContextEOClip(ctx); - - BOOL hasEqualColor = !_borderTopColor && !_borderRightColor && !_borderBottomColor && !_borderLeftColor; - if ((hasClipping || !hasRadius) && hasEqualColor) { - CGContextSetFillColorWithColor(ctx, _borderColor); - CGContextAddRect(ctx, rect); - CGContextAddPath(ctx, insetPath); - CGContextEOFillPath(ctx); - } else { - BOOL didSet = NO; - CGPoint topLeft; - if (innerTopLeftRadiusX > 0 && innerTopLeftRadiusY > 0) { - CGPoint points[2]; - RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, topWidth, 2 * innerTopLeftRadiusX, 2 * innerTopLeftRadiusY), CGPointMake(0, 0), CGPointMake(leftWidth, topWidth), points); - if (!isnan(points[1].x) && !isnan(points[1].y)) { - topLeft = points[1]; - didSet = YES; - } - } - - if (!didSet) { - topLeft = CGPointMake(leftWidth, topWidth); - } - - didSet = NO; - CGPoint bottomLeft; - if (innerBottomLeftRadiusX > 0 && innerBottomLeftRadiusY > 0) { - CGPoint points[2]; - RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, (size.height - bottomWidth) - 2 * innerBottomLeftRadiusY, 2 * innerBottomLeftRadiusX, 2 * innerBottomLeftRadiusY), CGPointMake(0, size.height), CGPointMake(leftWidth, size.height - bottomWidth), points); - if (!isnan(points[1].x) && !isnan(points[1].y)) { - bottomLeft = points[1]; - didSet = YES; - } - } - - if (!didSet) { - bottomLeft = CGPointMake(leftWidth, size.height - bottomWidth); - } - - didSet = NO; - CGPoint topRight; - if (innerTopRightRadiusX > 0 && innerTopRightRadiusY > 0) { - CGPoint points[2]; - RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * innerTopRightRadiusX, topWidth, 2 * innerTopRightRadiusX, 2 * innerTopRightRadiusY), CGPointMake(size.width, 0), CGPointMake(size.width - rightWidth, topWidth), points); - if (!isnan(points[0].x) && !isnan(points[0].y)) { - topRight = points[0]; - didSet = YES; - } - } - - if (!didSet) { - topRight = CGPointMake(size.width - rightWidth, topWidth); - } - - didSet = NO; - CGPoint bottomRight; - if (innerBottomRightRadiusX > 0 && innerBottomRightRadiusY > 0) { - CGPoint points[2]; - RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * innerBottomRightRadiusX, (size.height - bottomWidth) - 2 * innerBottomRightRadiusY, 2 * innerBottomRightRadiusX, 2 * innerBottomRightRadiusY), CGPointMake(size.width, size.height), CGPointMake(size.width - rightWidth, size.height - bottomWidth), points); - if (!isnan(points[0].x) && !isnan(points[0].y)) { - bottomRight = points[0]; - didSet = YES; - } - } - - if (!didSet) { - bottomRight = CGPointMake(size.width - rightWidth, size.height - bottomWidth); - } - - // RIGHT - if (rightWidth > 0) { - CGContextSaveGState(ctx); - - const CGPoint points[] = { - CGPointMake(size.width, 0), - topRight, - bottomRight, - CGPointMake(size.width, size.height), - }; - - CGContextSetFillColorWithColor(ctx, _borderRightColor ?: _borderColor); - CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); - CGContextFillPath(ctx); - - CGContextRestoreGState(ctx); - } - - // BOTTOM - if (bottomWidth > 0) { - CGContextSaveGState(ctx); - - const CGPoint points[] = { - CGPointMake(0, size.height), - bottomLeft, - bottomRight, - CGPointMake(size.width, size.height), - }; - - CGContextSetFillColorWithColor(ctx, _borderBottomColor ?: _borderColor); - CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); - CGContextFillPath(ctx); - - CGContextRestoreGState(ctx); - } - - // LEFT - if (leftWidth > 0) { - CGContextSaveGState(ctx); - - const CGPoint points[] = { - CGPointMake(0, 0), - topLeft, - bottomLeft, - CGPointMake(0, size.height), - }; - - CGContextSetFillColorWithColor(ctx, _borderLeftColor ?: _borderColor); - CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); - CGContextFillPath(ctx); - - CGContextRestoreGState(ctx); - } - - // TOP - if (topWidth > 0) { - CGContextSaveGState(ctx); - - const CGPoint points[] = { - CGPointMake(0, 0), - topLeft, - topRight, - CGPointMake(size.width, 0), - }; - - CGContextSetFillColorWithColor(ctx, _borderTopColor ?: _borderColor); - CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); - CGContextFillPath(ctx); - - CGContextRestoreGState(ctx); - } - } - - CGPathRelease(insetPath); - - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - *contentsCenter = CGRectMake(edgeInsets.left / size.width, edgeInsets.top / size.height, 1.0 / size.width, 1.0 / size.height); - return [image resizableImageWithCapInsets:edgeInsets]; +- (RCTBorderColors)borderColors +{ + return (RCTBorderColors){ + _borderTopColor ?: _borderColor, + _borderLeftColor ?: _borderColor, + _borderBottomColor ?: _borderColor, + _borderRightColor ?: _borderColor + }; } - (void)displayLayer:(CALayer *)layer { - CGRect contentsCenter = {.size = {1, 1}}; - UIImage *image = [self borderImage:&contentsCenter]; + const RCTCornerRadii cornerRadii = [self cornerRadii]; + const UIEdgeInsets borderInsets = [self bordersAsInsets]; + const RCTBorderColors borderColors = [self borderColors]; - if (image && RCTRunningInTestEnvironment()) { + BOOL useIOSBorderRendering = + !RCTRunningInTestEnvironment() && + RCTCornerRadiiAreEqual(cornerRadii) && + RCTBorderInsetsAreEqual(borderInsets) && + RCTBorderColorsAreEqual(borderColors); + + // TODO: A problem with this is that iOS draws borders in front of the content + // whereas CSS draws them behind the content. Also iOS clips to the outside of + // the border, but CSS clips to the inside. To solve this, we'll need to add + // a container view inside the main view to correctly clip the subviews. + + if (useIOSBorderRendering) { + layer.cornerRadius = cornerRadii.topLeft; + layer.borderColor = borderColors.left; + layer.borderWidth = borderInsets.left; + layer.backgroundColor = _backgroundColor.CGColor; + layer.contents = nil; + layer.needsDisplayOnBoundsChange = NO; + layer.mask = nil; + return; + } + + UIImage *image = RCTGetBorderImage([self cornerRadii], + [self bordersAsInsets], + [self borderColors], + _backgroundColor.CGColor, + self.clipsToBounds); + + const CGRect contentsCenter = ({ + CGSize size = image.size; + UIEdgeInsets insets = image.capInsets; + CGRectMake( + insets.left / size.width, + insets.top / size.height, + 1.0 / size.width, + 1.0 / size.height + ); + }); + + if (RCTRunningInTestEnvironment()) { const CGSize size = self.bounds.size; UIGraphicsBeginImageContextWithOptions(size, NO, image.scale); [image drawInRect:(CGRect){CGPointZero, size}]; @@ -690,12 +527,12 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) UIGraphicsEndImageContext(); } - layer.backgroundColor = [image ? [UIColor clearColor] : _backgroundColor CGColor]; + layer.backgroundColor = NULL; layer.contents = (id)image.CGImage; layer.contentsCenter = contentsCenter; - layer.contentsScale = image.scale ?: 1.0; + layer.contentsScale = image.scale; layer.magnificationFilter = kCAFilterNearest; - layer.needsDisplayOnBoundsChange = image != nil; + layer.needsDisplayOnBoundsChange = YES; [self updateClippingForLayer:layer]; } @@ -706,30 +543,19 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) CGFloat cornerRadius = 0; if (self.clipsToBounds) { - if (_borderRadius > 0 && _borderTopLeftRadius < 0 && _borderTopRightRadius < 0 && _borderBottomLeftRadius < 0 && _borderBottomRightRadius < 0) { - cornerRadius = _borderRadius; + + const RCTCornerRadii cornerRadii = [self cornerRadii]; + if (RCTCornerRadiiAreEqual(cornerRadii)) { + + cornerRadius = cornerRadii.topLeft; + } else { - const CGRect bounds = layer.bounds; - const CGFloat maxRadius = MIN(bounds.size.height, bounds.size.width); - const CGFloat radius = MAX(0, _borderRadius); - const CGFloat topLeftRadius = MIN(_borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius, maxRadius); - const CGFloat topRightRadius = MIN(_borderTopRightRadius >= 0 ? _borderTopRightRadius : radius, maxRadius); - const CGFloat bottomLeftRadius = MIN(_borderBottomLeftRadius >= 0 ? _borderBottomLeftRadius : radius, maxRadius); - const CGFloat bottomRightRadius = MIN(_borderBottomRightRadius >= 0 ? _borderBottomRightRadius : radius, maxRadius); - if (ABS(topLeftRadius - topRightRadius) < RCTViewBorderThreshold && - ABS(topLeftRadius - bottomLeftRadius) < RCTViewBorderThreshold && - ABS(topLeftRadius - bottomRightRadius) < RCTViewBorderThreshold) { - cornerRadius = topLeftRadius; - } else { - CAShapeLayer *shapeLayer = [CAShapeLayer layer]; - - CGPathRef path = RCTPathCreateWithRoundedRect(bounds, topLeftRadius, topLeftRadius, topRightRadius, topRightRadius, bottomLeftRadius, bottomLeftRadius, bottomRightRadius, bottomRightRadius, NULL); - shapeLayer.path = path; - CGPathRelease(path); - - mask = shapeLayer; - } + CAShapeLayer *shapeLayer = [CAShapeLayer layer]; + CGPathRef path = RCTPathCreateWithRoundedRect(self.bounds, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL); + shapeLayer.path = path; + CGPathRelease(path); + mask = shapeLayer; } } @@ -790,74 +616,3 @@ setBorderRadius(BottomLeft) setBorderRadius(BottomRight) @end - -static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise) -{ - CGFloat xScale = 1, yScale = 1, radius = 0; - if (xRadius != 0) { - xScale = 1; - yScale = yRadius / xRadius; - radius = xRadius; - } else if (yRadius != 0) { - xScale = xRadius / yRadius; - yScale = 1; - radius = yRadius; - } - - CGAffineTransform t = CGAffineTransformMakeTranslation(x, y); - t = CGAffineTransformScale(t, xScale, yScale); - if (m != NULL) { - t = CGAffineTransformConcat(t, *m); - } - - CGPathAddArc(path, &t, 0, 0, radius, startAngle, endAngle, clockwise); -} - -static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform) -{ - const CGFloat minX = CGRectGetMinX(rect); - const CGFloat minY = CGRectGetMinY(rect); - const CGFloat maxX = CGRectGetMaxX(rect); - const CGFloat maxY = CGRectGetMaxY(rect); - - CGMutablePathRef path = CGPathCreateMutable(); - RCTPathAddEllipticArc(path, transform, minX + topLeftRadiusX, minY + topLeftRadiusY, topLeftRadiusX, topLeftRadiusY, M_PI, 3 * M_PI_2, false); - RCTPathAddEllipticArc(path, transform, maxX - topRightRadiusX, minY + topRightRadiusY, topRightRadiusX, topRightRadiusY, 3 * M_PI_2, 0, false); - RCTPathAddEllipticArc(path, transform, maxX - bottomRightRadiusX, maxY - bottomRightRadiusY, bottomRightRadiusX, bottomRightRadiusY, 0, M_PI_2, false); - RCTPathAddEllipticArc(path, transform, minX + bottomLeftRadiusX, maxY - bottomLeftRadiusY, bottomLeftRadiusX, bottomLeftRadiusY, M_PI_2, M_PI, false); - CGPathCloseSubpath(path); - return path; -} - -static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2]) -{ - const CGFloat ellipseCenterX = CGRectGetMidX(ellipseBoundingRect); - const CGFloat ellipseCenterY = CGRectGetMidY(ellipseBoundingRect); - - // ellipseBoundingRect.origin.x -= ellipseCenterX; - // ellipseBoundingRect.origin.y -= ellipseCenterY; - - p1.x -= ellipseCenterX; - p1.y -= ellipseCenterY; - - p2.x -= ellipseCenterX; - p2.y -= ellipseCenterY; - - const CGFloat m = (p2.y - p1.y) / (p2.x - p1.x); - const CGFloat a = ellipseBoundingRect.size.width / 2; - const CGFloat b = ellipseBoundingRect.size.height / 2; - const CGFloat c = p1.y - m * p1.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] = CGPointMake(x1 + ellipseCenterX, y1 + ellipseCenterY); - intersections[1] = CGPointMake(x2 + ellipseCenterX, y2 + ellipseCenterY); - return YES; -}