diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png index 8ec70eb0ce8..4df43bb79e1 100644 Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png differ diff --git a/Examples/UIExplorer/ViewExample.js b/Examples/UIExplorer/ViewExample.js index c20d7e11996..deed55067e2 100644 --- a/Examples/UIExplorer/ViewExample.js +++ b/Examples/UIExplorer/ViewExample.js @@ -40,16 +40,6 @@ var ViewBorderStyleExample = React.createClass({ }, render() { - if (Platform.OS !== 'android') { - return ( - - - borderStyle is only supported on android for now. - - - ); - } - return ( diff --git a/React/Views/RCTBorderDrawing.h b/React/Views/RCTBorderDrawing.h index b67074337e7..52e3b4a86df 100644 --- a/React/Views/RCTBorderDrawing.h +++ b/React/Views/RCTBorderDrawing.h @@ -9,6 +9,8 @@ #import +#import "RCTBorderStyle.h" + typedef struct { CGFloat topLeft; CGFloat topRight; @@ -39,6 +41,7 @@ BOOL RCTBorderColorsAreEqual(RCTBorderColors borderColors); /** * Convert RCTCornerRadii to RCTCornerInsets by applying border insets. + * Effectively, returns radius - inset, with a lower bound of 0.0. */ RCTCornerInsets RCTGetCornerInsets(RCTCornerRadii cornerRadii, UIEdgeInsets borderInsets); @@ -52,9 +55,14 @@ CGPathRef RCTPathCreateWithRoundedRect(CGRect bounds, const CGAffineTransform *transform); /** - * Draw a CSS-compliant border as a scalable image. + * Draw a CSS-compliant border as an image. You can determine if it's scalable + * by inspecting the image's `capInsets`. + * + * `borderInsets` defines the border widths for each edge. */ -UIImage *RCTGetBorderImage(RCTCornerRadii cornerRadii, +UIImage *RCTGetBorderImage(RCTBorderStyle borderStyle, + CGSize viewSize, + RCTCornerRadii cornerRadii, UIEdgeInsets borderInsets, RCTBorderColors borderColors, CGColorRef backgroundColor, diff --git a/React/Views/RCTBorderDrawing.m b/React/Views/RCTBorderDrawing.m index 8d9e7c0d4e2..e7f236bf698 100644 --- a/React/Views/RCTBorderDrawing.m +++ b/React/Views/RCTBorderDrawing.m @@ -8,6 +8,7 @@ */ #import "RCTBorderDrawing.h" +#import "RCTLog.h" static const CGFloat RCTViewBorderThreshold = 0.001; @@ -150,18 +151,35 @@ static void RCTEllipseGetIntersectionsWithLine(CGRect ellipseBounds, 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; +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 CGContextRef RCTUIGraphicsBeginImageContext(CGSize size, CGColorRef backgroundColor, BOOL hasCornerRadii, BOOL drawToEdge) { + const CGFloat alpha = CGColorGetAlpha(backgroundColor); + const BOOL opaque = (drawToEdge || !hasCornerRadii) && alpha == 1.0; + UIGraphicsBeginImageContextWithOptions(size, opaque, 0.0); + return UIGraphicsGetCurrentContext(); +} + +static UIImage *RCTGetSolidBorderImage(RCTCornerRadii cornerRadii, + UIEdgeInsets borderInsets, + RCTBorderColors borderColors, + CGColorRef backgroundColor, + BOOL drawToEdge) +{ + const BOOL hasCornerRadii = RCTCornerRadiiAreAboveThreshold(cornerRadii); const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, borderInsets); const UIEdgeInsets edgeInsets = (UIEdgeInsets){ @@ -172,23 +190,14 @@ UIImage *RCTGetBorderImage(RCTCornerRadii cornerRadii, }; const CGSize size = (CGSize){ + // 1pt for the middle stretchable area along each axis 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(); + CGContextRef ctx = RCTUIGraphicsBeginImageContext(size, backgroundColor, hasCornerRadii, drawToEdge); const CGRect rect = {.size = size}; - - CGPathRef path; - if (drawToEdge) { - path = CGPathCreateWithRect(rect, NULL); - } else { - path = RCTPathCreateWithRoundedRect(rect, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL); - } + CGPathRef path = RCTPathCreateOuterOutline(drawToEdge, rect, cornerRadii); if (backgroundColor) { CGContextSetFillColorWithColor(ctx, backgroundColor); @@ -329,3 +338,144 @@ UIImage *RCTGetBorderImage(RCTCornerRadii cornerRadii, return [image resizableImageWithCapInsets:edgeInsets]; } + +// 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 discontinous 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); + CGContextRef ctx = RCTUIGraphicsBeginImageContext(viewSize, backgroundColor, hasCornerRadii, drawToEdge); + const CGRect rect = {.size = viewSize}; + + if (backgroundColor) { + CGPathRef outerPath = RCTPathCreateOuterOutline(drawToEdge, rect, cornerRadii); + CGContextAddPath(ctx, outerPath); + CGPathRelease(outerPath); + + CGContextSetFillColorWithColor(ctx, backgroundColor); + CGContextFillPath(ctx); + } + + // 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(ctx, lineWidth); + CGContextSetLineDash(ctx, 0, dashLengths, sizeof(dashLengths) / sizeof(*dashLengths)); + + CGContextSetStrokeColorWithColor(ctx, [UIColor yellowColor].CGColor); + + CGContextAddPath(ctx, path); + CGContextSetStrokeColorWithColor(ctx, borderColors.top); + CGContextStrokePath(ctx); + + CGPathRelease(path); + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; +} + +UIImage *RCTGetBorderImage(RCTBorderStyle borderStyle, + CGSize viewSize, + RCTCornerRadii cornerRadii, + UIEdgeInsets borderInsets, + RCTBorderColors borderColors, + CGColorRef backgroundColor, + BOOL drawToEdge) +{ + + switch (borderStyle) { + case RCTBorderStyleSolid: + return RCTGetSolidBorderImage(cornerRadii, borderInsets, borderColors, backgroundColor, drawToEdge); + case RCTBorderStyleDashed: + case RCTBorderStyleDotted: + return RCTGetDashedOrDottedBorderImage(borderStyle, cornerRadii, viewSize, borderInsets, borderColors, backgroundColor, drawToEdge); + case RCTBorderStyleUnset: + break; + } + + return nil; +} diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 3a3ea732548..b2cd7a25083 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -533,12 +533,22 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) return; } - UIImage *image = RCTGetBorderImage(cornerRadii, + UIImage *image = RCTGetBorderImage(_borderStyle, + layer.bounds.size, + cornerRadii, borderInsets, borderColors, _backgroundColor.CGColor, self.clipsToBounds); + layer.backgroundColor = NULL; + + if (image == nil) { + layer.contents = nil; + layer.needsDisplayOnBoundsChange = NO; + return; + } + CGRect contentsCenter = ({ CGSize size = image.size; UIEdgeInsets insets = image.capInsets; @@ -559,12 +569,17 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) contentsCenter = CGRectMake(0, 0, 1, 1); } - layer.backgroundColor = NULL; layer.contents = (id)image.CGImage; - layer.contentsCenter = contentsCenter; layer.contentsScale = image.scale; - layer.magnificationFilter = kCAFilterNearest; layer.needsDisplayOnBoundsChange = YES; + layer.magnificationFilter = kCAFilterNearest; + + const BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); + if (isResizable) { + layer.contentsCenter = contentsCenter; + } else { + layer.contentsCenter = CGRectMake(0.0, 0.0, 1.0, 1.0); + } [self updateClippingForLayer:layer]; }