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:
parent
4472bb54c9
commit
15aa146255
Binary file not shown.
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 153 KiB |
|
@ -40,16 +40,6 @@ var ViewBorderStyleExample = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
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 (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={this._handlePress}>
|
<TouchableWithoutFeedback onPress={this._handlePress}>
|
||||||
<View>
|
<View>
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
#import "RCTBorderStyle.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
CGFloat topLeft;
|
CGFloat topLeft;
|
||||||
CGFloat topRight;
|
CGFloat topRight;
|
||||||
|
@ -39,6 +41,7 @@ BOOL RCTBorderColorsAreEqual(RCTBorderColors borderColors);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert RCTCornerRadii to RCTCornerInsets by applying border insets.
|
* Convert RCTCornerRadii to RCTCornerInsets by applying border insets.
|
||||||
|
* Effectively, returns radius - inset, with a lower bound of 0.0.
|
||||||
*/
|
*/
|
||||||
RCTCornerInsets RCTGetCornerInsets(RCTCornerRadii cornerRadii,
|
RCTCornerInsets RCTGetCornerInsets(RCTCornerRadii cornerRadii,
|
||||||
UIEdgeInsets borderInsets);
|
UIEdgeInsets borderInsets);
|
||||||
|
@ -52,9 +55,14 @@ CGPathRef RCTPathCreateWithRoundedRect(CGRect bounds,
|
||||||
const CGAffineTransform *transform);
|
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,
|
UIEdgeInsets borderInsets,
|
||||||
RCTBorderColors borderColors,
|
RCTBorderColors borderColors,
|
||||||
CGColorRef backgroundColor,
|
CGColorRef backgroundColor,
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import "RCTBorderDrawing.h"
|
#import "RCTBorderDrawing.h"
|
||||||
|
#import "RCTLog.h"
|
||||||
|
|
||||||
static const CGFloat RCTViewBorderThreshold = 0.001;
|
static const CGFloat RCTViewBorderThreshold = 0.001;
|
||||||
|
|
||||||
|
@ -150,18 +151,35 @@ static void RCTEllipseGetIntersectionsWithLine(CGRect ellipseBounds,
|
||||||
intersections[1] = (CGPoint){x2 + ellipseCenter.x, y2 + ellipseCenter.y};
|
intersections[1] = (CGPoint){x2 + ellipseCenter.x, y2 + ellipseCenter.y};
|
||||||
}
|
}
|
||||||
|
|
||||||
UIImage *RCTGetBorderImage(RCTCornerRadii cornerRadii,
|
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,
|
UIEdgeInsets borderInsets,
|
||||||
RCTBorderColors borderColors,
|
RCTBorderColors borderColors,
|
||||||
CGColorRef backgroundColor,
|
CGColorRef backgroundColor,
|
||||||
BOOL drawToEdge)
|
BOOL drawToEdge)
|
||||||
{
|
{
|
||||||
const BOOL hasCornerRadii =
|
const BOOL hasCornerRadii = RCTCornerRadiiAreAboveThreshold(cornerRadii);
|
||||||
cornerRadii.topLeft > RCTViewBorderThreshold ||
|
|
||||||
cornerRadii.topRight > RCTViewBorderThreshold ||
|
|
||||||
cornerRadii.bottomLeft > RCTViewBorderThreshold ||
|
|
||||||
cornerRadii.bottomRight > RCTViewBorderThreshold;
|
|
||||||
|
|
||||||
const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, borderInsets);
|
const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, borderInsets);
|
||||||
|
|
||||||
const UIEdgeInsets edgeInsets = (UIEdgeInsets){
|
const UIEdgeInsets edgeInsets = (UIEdgeInsets){
|
||||||
|
@ -172,23 +190,14 @@ UIImage *RCTGetBorderImage(RCTCornerRadii cornerRadii,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CGSize size = (CGSize){
|
const CGSize size = (CGSize){
|
||||||
|
// 1pt for the middle stretchable area along each axis
|
||||||
edgeInsets.left + 1 + edgeInsets.right,
|
edgeInsets.left + 1 + edgeInsets.right,
|
||||||
edgeInsets.top + 1 + edgeInsets.bottom
|
edgeInsets.top + 1 + edgeInsets.bottom
|
||||||
};
|
};
|
||||||
|
|
||||||
const CGFloat alpha = CGColorGetAlpha(backgroundColor);
|
CGContextRef ctx = RCTUIGraphicsBeginImageContext(size, backgroundColor, hasCornerRadii, drawToEdge);
|
||||||
const BOOL opaque = (drawToEdge || !hasCornerRadii) && alpha == 1.0;
|
|
||||||
UIGraphicsBeginImageContextWithOptions(size, opaque, 0.0);
|
|
||||||
|
|
||||||
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
||||||
const CGRect rect = {.size = size};
|
const CGRect rect = {.size = size};
|
||||||
|
CGPathRef path = RCTPathCreateOuterOutline(drawToEdge, rect, cornerRadii);
|
||||||
CGPathRef path;
|
|
||||||
if (drawToEdge) {
|
|
||||||
path = CGPathCreateWithRect(rect, NULL);
|
|
||||||
} else {
|
|
||||||
path = RCTPathCreateWithRoundedRect(rect, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backgroundColor) {
|
if (backgroundColor) {
|
||||||
CGContextSetFillColorWithColor(ctx, backgroundColor);
|
CGContextSetFillColorWithColor(ctx, backgroundColor);
|
||||||
|
@ -329,3 +338,144 @@ UIImage *RCTGetBorderImage(RCTCornerRadii cornerRadii,
|
||||||
|
|
||||||
return [image resizableImageWithCapInsets:edgeInsets];
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -533,12 +533,22 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UIImage *image = RCTGetBorderImage(cornerRadii,
|
UIImage *image = RCTGetBorderImage(_borderStyle,
|
||||||
|
layer.bounds.size,
|
||||||
|
cornerRadii,
|
||||||
borderInsets,
|
borderInsets,
|
||||||
borderColors,
|
borderColors,
|
||||||
_backgroundColor.CGColor,
|
_backgroundColor.CGColor,
|
||||||
self.clipsToBounds);
|
self.clipsToBounds);
|
||||||
|
|
||||||
|
layer.backgroundColor = NULL;
|
||||||
|
|
||||||
|
if (image == nil) {
|
||||||
|
layer.contents = nil;
|
||||||
|
layer.needsDisplayOnBoundsChange = NO;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CGRect contentsCenter = ({
|
CGRect contentsCenter = ({
|
||||||
CGSize size = image.size;
|
CGSize size = image.size;
|
||||||
UIEdgeInsets insets = image.capInsets;
|
UIEdgeInsets insets = image.capInsets;
|
||||||
|
@ -559,12 +569,17 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
||||||
contentsCenter = CGRectMake(0, 0, 1, 1);
|
contentsCenter = CGRectMake(0, 0, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
layer.backgroundColor = NULL;
|
|
||||||
layer.contents = (id)image.CGImage;
|
layer.contents = (id)image.CGImage;
|
||||||
layer.contentsCenter = contentsCenter;
|
|
||||||
layer.contentsScale = image.scale;
|
layer.contentsScale = image.scale;
|
||||||
layer.magnificationFilter = kCAFilterNearest;
|
|
||||||
layer.needsDisplayOnBoundsChange = YES;
|
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];
|
[self updateClippingForLayer:layer];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue