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];
}