Support dashed and dotted border styles on iOS

Summary:
Support dashed and dotted border styles on iOS

public

Reviewed By: nicklockwood

Differential Revision: D2773579

fb-gh-sync-id: f4b99943f38e849602295a86bdb1780c0abbc8e8
This commit is contained in:
Milen Dzhumerov 2015-12-22 08:30:00 -08:00 committed by facebook-github-bot-3
parent 4472bb54c9
commit 15aa146255
5 changed files with 202 additions and 39 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 153 KiB

View File

@ -40,16 +40,6 @@ var ViewBorderStyleExample = React.createClass({
},
render() {
if (Platform.OS !== 'android') {
return (
<View style={{backgroundColor: 'red'}}>
<Text style={{color: 'white'}}>
borderStyle is only supported on android for now.
</Text>
</View>
);
}
return (
<TouchableWithoutFeedback onPress={this._handlePress}>
<View>

View File

@ -9,6 +9,8 @@
#import <UIKit/UIKit.h>
#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,

View File

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

View File

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