diff --git a/Examples/UIExplorer/BorderExample.js b/Examples/UIExplorer/BorderExample.js new file mode 100644 index 000000000..668e77880 --- /dev/null +++ b/Examples/UIExplorer/BorderExample.js @@ -0,0 +1,90 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + View +} = React; + +var styles = StyleSheet.create({ + box: { + width: 100, + height: 100, + }, + border1: { + borderWidth: 10, + borderColor: 'brown', + }, + borderRadius: { + borderWidth: 10, + borderRadius: 10, + borderColor: 'cyan', + }, + border2: { + borderWidth: 10, + borderTopColor: 'red', + borderRightColor: 'yellow', + borderBottomColor: 'green', + borderLeftColor: 'blue', + }, + border3: { + borderColor: 'purple', + borderTopWidth: 10, + borderRightWidth: 20, + borderBottomWidth: 30, + borderLeftWidth: 40, + }, + border4: { + borderTopWidth: 10, + borderTopColor: 'red', + borderRightWidth: 20, + borderRightColor: 'yellow', + borderBottomWidth: 30, + borderBottomColor: 'green', + borderLeftWidth: 40, + borderLeftColor: 'blue', + }, +}); + +exports.title = 'Border'; +exports.description = 'View borders'; +exports.examples = [ + { + title: 'Equal-Width / Same-Color', + description: 'borderWidth & borderColor', + render() { + return ; + } + }, + { + title: 'Equal-Width / Same-Color', + description: 'borderWidth & borderColor', + render() { + return ; + } + }, + { + title: 'Equal-Width Borders', + description: 'borderWidth & border*Color', + render() { + return ; + } + }, + { + title: 'Same-Color Borders', + description: 'border*Width & borderColor', + render() { + return ; + } + }, + { + title: 'Custom Borders', + description: 'border*Width & border*Color', + render() { + return ; + } + }, +]; diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index ce3d6e746..037fb3dcd 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -55,6 +55,7 @@ var APIS = [ require('./AlertIOSExample'), require('./AppStateIOSExample'), require('./AsyncStorageExample'), + require('./BorderExample'), require('./CameraRollExample.ios'), require('./GeolocationExample'), require('./LayoutExample'), diff --git a/ReactKit/Modules/RCTUIManager.m b/ReactKit/Modules/RCTUIManager.m index 8c3274910..d8645d612 100644 --- a/ReactKit/Modules/RCTUIManager.m +++ b/ReactKit/Modules/RCTUIManager.m @@ -437,9 +437,6 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName) completion(YES); } - // TODO: deprecate this - [view reactSetBorders]; - // Animate view creation BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; RCTAnimation *createAnimation = _layoutAnimation.createAnimation; diff --git a/ReactKit/Views/RCTTabBarItemManager.m b/ReactKit/Views/RCTTabBarItemManager.m index 227a4ef40..7f4ce9642 100644 --- a/ReactKit/Views/RCTTabBarItemManager.m +++ b/ReactKit/Views/RCTTabBarItemManager.m @@ -21,7 +21,7 @@ RCT_EXPORT_VIEW_PROPERTY(selected, BOOL); RCT_EXPORT_VIEW_PROPERTY(icon, NSString); -RCT_REMAP_VIEW_PROPERTY(selectedIcon, barItem.selectedImage, NSString); +RCT_REMAP_VIEW_PROPERTY(selectedIcon, barItem.selectedImage, UIImage); RCT_REMAP_VIEW_PROPERTY(badgeValue, barItem.badgeValue, NSString); RCT_CUSTOM_VIEW_PROPERTY(title, NSString, RCTTabBarItem) { diff --git a/ReactKit/Views/RCTView.h b/ReactKit/Views/RCTView.h index 4d99c876f..73fe2c7cb 100644 --- a/ReactKit/Views/RCTView.h +++ b/ReactKit/Views/RCTView.h @@ -13,6 +13,13 @@ #import "RCTPointerEvents.h" +typedef NS_ENUM(NSInteger, RCTBorderSide) { + RCTBorderSideTop, + RCTBorderSideRight, + RCTBorderSideBottom, + RCTBorderSideLeft +}; + @protocol RCTAutoInsetsProtocol; @interface RCTView : UIView @@ -48,4 +55,22 @@ */ - (void)updateClippedSubviews; +/** + * Border colors. + */ +@property (nonatomic, assign) CGColorRef borderTopColor; +@property (nonatomic, assign) CGColorRef borderRightColor; +@property (nonatomic, assign) CGColorRef borderBottomColor; +@property (nonatomic, assign) CGColorRef borderLeftColor; +@property (nonatomic, assign) CGColorRef borderColor; + +/** + * Border widths. + */ +@property (nonatomic, assign) CGFloat borderTopWidth; +@property (nonatomic, assign) CGFloat borderRightWidth; +@property (nonatomic, assign) CGFloat borderBottomWidth; +@property (nonatomic, assign) CGFloat borderLeftWidth; +@property (nonatomic, assign) CGFloat borderWidth; + @end diff --git a/ReactKit/Views/RCTView.m b/ReactKit/Views/RCTView.m index 5bb13ab86..7d4d12aac 100644 --- a/ReactKit/Views/RCTView.m +++ b/ReactKit/Views/RCTView.m @@ -14,6 +14,8 @@ #import "RCTLog.h" #import "UIView+ReactKit.h" +static const RCTBorderSide RCTBorderSideCount = 4; + @implementation UIView (RCTViewUnmounting) - (void)react_remountAllSubviews @@ -91,6 +93,8 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) @implementation RCTView { NSMutableArray *_reactSubviews; + CAShapeLayer *_borderLayers[RCTBorderSideCount]; + CGFloat _borderWidths[RCTBorderSideCount]; } - (NSString *)accessibilityLabel @@ -106,7 +110,7 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) _pointerEvents = pointerEvents; self.userInteractionEnabled = (pointerEvents != RCTPointerEventsNone); if (pointerEvents == RCTPointerEventsBoxNone) { - self.accessibilityViewIsModal = NO; // TODO: find out what this is for + self.accessibilityViewIsModal = NO; } } @@ -368,9 +372,193 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) // to updateClippedSubviews manually after loading [super layoutSubviews]; + if (_reactSubviews) { [self updateClippedSubviews]; } + + for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { + if (_borderLayers[side]) [self updatePathForShapeLayerForSide:side]; + } +} + +- (void)layoutSublayersOfLayer:(CALayer *)layer +{ + [super layoutSublayersOfLayer:layer]; + + const CGRect bounds = layer.bounds; + for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { + _borderLayers[side].frame = bounds; + } +} + +- (BOOL)getTrapezoidPoints:(CGPoint[4])outPoints forSide:(RCTBorderSide)side +{ + const CGRect bounds = self.layer.bounds; + const CGFloat minX = CGRectGetMinX(bounds); + const CGFloat maxX = CGRectGetMaxX(bounds); + const CGFloat minY = CGRectGetMinY(bounds); + const CGFloat maxY = CGRectGetMaxY(bounds); + +#define BW(SIDE) [self borderWidthForSide:RCTBorderSide##SIDE] + + switch (side) { + case RCTBorderSideRight: + outPoints[0] = CGPointMake(maxX - BW(Right), maxY - BW(Bottom)); + outPoints[1] = CGPointMake(maxX - BW(Right), minY + BW(Top)); + outPoints[2] = CGPointMake(maxX, minY); + outPoints[3] = CGPointMake(maxX, maxY); + break; + case RCTBorderSideBottom: + outPoints[0] = CGPointMake(minX + BW(Left), maxY - BW(Bottom)); + outPoints[1] = CGPointMake(maxX - BW(Right), maxY - BW(Bottom)); + outPoints[2] = CGPointMake(maxX, maxY); + outPoints[3] = CGPointMake(minX, maxY); + break; + case RCTBorderSideLeft: + outPoints[0] = CGPointMake(minX + BW(Left), minY + BW(Top)); + outPoints[1] = CGPointMake(minX + BW(Left), maxY - BW(Bottom)); + outPoints[2] = CGPointMake(minX, maxY); + outPoints[3] = CGPointMake(minX, minY); + break; + case RCTBorderSideTop: + outPoints[0] = CGPointMake(maxX - BW(Right), minY + BW(Top)); + outPoints[1] = CGPointMake(minX + BW(Left), minY + BW(Top)); + outPoints[2] = CGPointMake(minX, minY); + outPoints[3] = CGPointMake(maxX, minY); + break; + } + + return YES; +} + +- (CAShapeLayer *)createShapeLayerIfNotExistsForSide:(RCTBorderSide)side +{ + CAShapeLayer *borderLayer = _borderLayers[side]; + if (!borderLayer) { + borderLayer = [CAShapeLayer layer]; + borderLayer.fillColor = self.layer.borderColor; + [self.layer addSublayer:borderLayer]; + _borderLayers[side] = borderLayer; + } + return borderLayer; +} + +- (void)updatePathForShapeLayerForSide:(RCTBorderSide)side +{ + CAShapeLayer *borderLayer = [self createShapeLayerIfNotExistsForSide:side]; + + CGPoint trapezoidPoints[4]; + [self getTrapezoidPoints:trapezoidPoints forSide:side]; + + CGMutablePathRef path = CGPathCreateMutable(); + CGPathAddLines(path, NULL, trapezoidPoints, 4); + CGPathCloseSubpath(path); + borderLayer.path = path; + CGPathRelease(path); +} + +- (void)updateBorderLayers +{ + BOOL widthsAndColorsSame = YES; + CGFloat width = _borderWidths[0]; + CGColorRef color = _borderLayers[0].fillColor; + for (RCTBorderSide side = 1; side < RCTBorderSideCount; side++) { + CAShapeLayer *layer = _borderLayers[side]; + if (_borderWidths[side] != width || (layer && !CGColorEqualToColor(layer.fillColor, color))) { + widthsAndColorsSame = NO; + break; + } + } + if (widthsAndColorsSame) { + + // Set main layer border + if (width) { + _borderWidth = self.layer.borderWidth = width; + } + if (color) { + self.layer.borderColor = color; + } + + // Remove border layers + for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { + [_borderLayers[side] removeFromSuperlayer]; + _borderLayers[side] = nil; + } + + } else { + + // Clear main layer border + self.layer.borderWidth = 0; + + // Set up border layers + for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { + [self updatePathForShapeLayerForSide:side]; + } + } +} + +- (CGFloat)borderWidthForSide:(RCTBorderSide)side +{ + return _borderWidths[side] ?: _borderWidth; +} + +- (void)setBorderWidth:(CGFloat)width forSide:(RCTBorderSide)side +{ + _borderWidths[side] = width; + [self updateBorderLayers]; +} + +#define BORDER_WIDTH(SIDE) \ +- (CGFloat)border##SIDE##Width { return [self borderWidthForSide:RCTBorderSide##SIDE]; } \ +- (void)setBorder##SIDE##Width:(CGFloat)width { [self setBorderWidth:width forSide:RCTBorderSide##SIDE]; } + +BORDER_WIDTH(Top) +BORDER_WIDTH(Right) +BORDER_WIDTH(Bottom) +BORDER_WIDTH(Left) + +- (CGColorRef)borderColorForSide:(RCTBorderSide)side +{ + return _borderLayers[side].fillColor ?: self.layer.borderColor; +} + +- (void)setBorderColor:(CGColorRef)color forSide:(RCTBorderSide)side +{ + [self createShapeLayerIfNotExistsForSide:side].fillColor = color; + [self updateBorderLayers]; +} + +#define BORDER_COLOR(SIDE) \ +- (CGColorRef)border##SIDE##Color { return [self borderColorForSide:RCTBorderSide##SIDE]; } \ +- (void)setBorder##SIDE##Color:(CGColorRef)color { [self setBorderColor:color forSide:RCTBorderSide##SIDE]; } + +BORDER_COLOR(Top) +BORDER_COLOR(Right) +BORDER_COLOR(Bottom) +BORDER_COLOR(Left) + +- (void)setBorderWidth:(CGFloat)borderWidth +{ + _borderWidth = borderWidth; + for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { + _borderWidths[side] = borderWidth; + } + [self updateBorderLayers]; +} + +- (void)setBorderColor:(CGColorRef)borderColor +{ + self.layer.borderColor = borderColor; + for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) { + _borderLayers[side].fillColor = borderColor; + } + [self updateBorderLayers]; +} + +- (CGColorRef)borderColor +{ + return self.layer.borderColor; } @end diff --git a/ReactKit/Views/RCTViewManager.m b/ReactKit/Views/RCTViewManager.m index 943a8e43c..79a9c22da 100644 --- a/ReactKit/Views/RCTViewManager.m +++ b/ReactKit/Views/RCTViewManager.m @@ -81,9 +81,6 @@ RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor); RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset, CGSize); RCT_REMAP_VIEW_PROPERTY(shadowOpacity, layer.shadowOpacity, CGFloat) RCT_REMAP_VIEW_PROPERTY(shadowRadius, layer.shadowRadius, CGFloat) -RCT_REMAP_VIEW_PROPERTY(borderColor, layer.borderColor, CGColor); -RCT_REMAP_VIEW_PROPERTY(borderRadius, layer.cornerRadius, CGFloat) -RCT_REMAP_VIEW_PROPERTY(borderWidth, layer.borderWidth, CGFloat) RCT_REMAP_VIEW_PROPERTY(transformMatrix, layer.transform, CATransform3D) RCT_CUSTOM_VIEW_PROPERTY(overflow, css_overflow, RCTView) { @@ -122,6 +119,42 @@ RCT_CUSTOM_VIEW_PROPERTY(removeClippedSubviews, BOOL, RCTView) view.removeClippedSubviews = json ? [RCTConvert BOOL:json] : defaultView.removeClippedSubviews; } } +RCT_REMAP_VIEW_PROPERTY(borderRadius, layer.cornerRadius, CGFloat) +RCT_CUSTOM_VIEW_PROPERTY(borderColor, CGColor, RCTView) +{ + if ([view respondsToSelector:@selector(setBorderColor:)]) { + view.borderColor = json ? [RCTConvert CGColor:json] : defaultView.borderColor; + } else { + view.layer.borderColor = json ? [RCTConvert CGColor:json] : defaultView.layer.borderColor; + } +} +RCT_CUSTOM_VIEW_PROPERTY(borderWidth, CGFloat, RCTView) +{ + if ([view respondsToSelector:@selector(setBorderWidth:)]) { + view.borderWidth = json ? [RCTConvert CGFloat:json] : defaultView.borderWidth; + } else { + view.layer.borderWidth = json ? [RCTConvert CGFloat:json] : defaultView.layer.borderWidth; + } +} + +#define RCT_VIEW_BORDER_PROPERTY(SIDE) \ +RCT_CUSTOM_VIEW_PROPERTY(border##SIDE##Width, CGFloat, RCTView) \ +{ \ + if ([view respondsToSelector:@selector(setBorder##SIDE##Width:)]) { \ + view.border##SIDE##Width = json ? [RCTConvert CGFloat:json] : defaultView.border##SIDE##Width; \ + } \ +} \ +RCT_CUSTOM_VIEW_PROPERTY(border##SIDE##Color, UIColor, RCTView) \ +{ \ + if ([view respondsToSelector:@selector(setBorder##SIDE##Color:)]) { \ + view.border##SIDE##Color = json ? [RCTConvert CGColor:json] : defaultView.border##SIDE##Color; \ + } \ +} + +RCT_VIEW_BORDER_PROPERTY(Top) +RCT_VIEW_BORDER_PROPERTY(Right) +RCT_VIEW_BORDER_PROPERTY(Bottom) +RCT_VIEW_BORDER_PROPERTY(Left) #pragma mark - ShadowView properties @@ -169,15 +202,4 @@ RCT_CUSTOM_SHADOW_PROPERTY(backgroundColor, UIColor, RCTShadowView) view.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; } -// Border properties - to be deprecated - -RCT_REMAP_VIEW_PROPERTY(borderTopWidth, reactBorderTop.width, CGFloat); -RCT_REMAP_VIEW_PROPERTY(borderRightWidth, reactBorderRight.width, CGFloat); -RCT_REMAP_VIEW_PROPERTY(borderBottomWidth, reactBorderBottom.width, CGFloat); -RCT_REMAP_VIEW_PROPERTY(borderLeftWidth, reactBorderLeft.width, CGFloat); -RCT_REMAP_VIEW_PROPERTY(borderTopColor, reactBorderTop.color, UIColor); -RCT_REMAP_VIEW_PROPERTY(borderRightColor, reactBorderRight.color, UIColor); -RCT_REMAP_VIEW_PROPERTY(borderBottomColor, reactBorderBottom.color, UIColor); -RCT_REMAP_VIEW_PROPERTY(borderLeftColor, reactBorderLeft.color, UIColor); - @end diff --git a/ReactKit/Views/UIView+ReactKit.h b/ReactKit/Views/UIView+ReactKit.h index b2ba2ca9a..ab978e58d 100644 --- a/ReactKit/Views/UIView+ReactKit.h +++ b/ReactKit/Views/UIView+ReactKit.h @@ -42,12 +42,3 @@ - (BOOL)reactRespondsToTouch:(UITouch *)touch; @end - -@interface UIView (ReactKitBorders) - -/** - * Borders stuff - pay no attention to this, it's going away (#6548297) - */ -- (void)reactSetBorders; - -@end diff --git a/ReactKit/Views/UIView+ReactKit.m b/ReactKit/Views/UIView+ReactKit.m index 2ef2ce31b..0ea1df053 100644 --- a/ReactKit/Views/UIView+ReactKit.m +++ b/ReactKit/Views/UIView+ReactKit.m @@ -119,156 +119,3 @@ } @end - -#pragma mark - Borders - -// Note: the value of this enum determines their relative zPosition -typedef NS_ENUM(NSUInteger, RCTBorderSide) { - RCTBorderSideTop = 0, - RCTBorderSideRight = 1, - RCTBorderSideBottom = 2, - RCTBorderSideLeft = 3 -}; - -@interface RCTSingleSidedBorder : NSObject - -@property (nonatomic, readwrite, assign) CGFloat width; -@property (nonatomic, readwrite, strong) UIColor *color; -@property (nonatomic, readonly, assign) RCTBorderSide side; - -- (instancetype)initWithSide:(RCTBorderSide)side superlayer:(CALayer *)superlayer; - -- (void)superLayerBoundsDidChange; - -@end - -@implementation RCTSingleSidedBorder -{ - CALayer *_borderLayer; -} - -- (instancetype)initWithSide:(RCTBorderSide)side superlayer:(CALayer *)superlayer -{ - if (self = [super init]) { - _side = side; - - _borderLayer = [CALayer layer]; - _borderLayer.delegate = self; - _borderLayer.zPosition = INT_MAX - _side; - - [superlayer insertSublayer:_borderLayer atIndex:0]; - } - return self; -} - -- (void)dealloc -{ - _borderLayer.delegate = nil; -} - -- (void)setWidth:(CGFloat)width -{ - _width = width; - [_borderLayer setNeedsLayout]; -} - -- (void)setColor:(UIColor *)color -{ - _color = color; - _borderLayer.backgroundColor = _color.CGColor; - [_borderLayer setNeedsLayout]; -} - -- (void)superLayerBoundsDidChange -{ - [_borderLayer setNeedsLayout]; -} - -#pragma mark - CALayerDelegate - -- (void)layoutSublayersOfLayer:(CALayer *)layer -{ - CGSize superlayerSize = layer.superlayer.frame.size; - - CGFloat xPosition = 0.0f; - CGFloat yPosition = 0.0f; - - // Note: we ensure side layers are below top & bottom for snapshot test consistency - - switch (self.side) { - case RCTBorderSideTop: - layer.frame = CGRectMake(xPosition, yPosition, superlayerSize.width, self.width); - break; - case RCTBorderSideRight: - xPosition = superlayerSize.width - self.width; - layer.frame = CGRectMake(xPosition, yPosition, self.width, superlayerSize.height); - [layer.superlayer insertSublayer:layer atIndex:0]; - break; - case RCTBorderSideBottom: - yPosition = superlayerSize.height - self.width; - layer.frame = CGRectMake(xPosition, yPosition, superlayerSize.width, self.width); - break; - case RCTBorderSideLeft: - layer.frame = CGRectMake(xPosition, yPosition, self.width, superlayerSize.height); - [layer.superlayer insertSublayer:layer atIndex:0]; - break; - } -} - -// Disable animations for layer -- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event -{ - return (id)[NSNull null]; -} - -@end - -@implementation UIView (ReactKitBorders) - -- (void)reactSetBorders -{ - NSMutableDictionary *borders = objc_getAssociatedObject(self, @selector(_createOrGetBorderWithSide:)); - if (borders) { - for (RCTSingleSidedBorder *border in [borders allValues]) { - [border superLayerBoundsDidChange]; - } - } -} - -- (RCTSingleSidedBorder *)reactBorderTop -{ - return [self _createOrGetBorderWithSide:RCTBorderSideTop]; -} - -- (RCTSingleSidedBorder *)reactBorderRight -{ - return [self _createOrGetBorderWithSide:RCTBorderSideRight]; -} - -- (RCTSingleSidedBorder *)reactBorderBottom -{ - return [self _createOrGetBorderWithSide:RCTBorderSideBottom]; -} - -- (RCTSingleSidedBorder *)reactBorderLeft -{ - return [self _createOrGetBorderWithSide:RCTBorderSideLeft]; -} - -- (RCTSingleSidedBorder *)_createOrGetBorderWithSide:(RCTBorderSide)side -{ - NSMutableDictionary *borders = objc_getAssociatedObject(self, _cmd); - if (!borders) { - borders = [[NSMutableDictionary alloc] init]; - objc_setAssociatedObject(self, _cmd, borders, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - - RCTSingleSidedBorder *border = [borders objectForKey:@(side)]; - if (!border) { - border = [[RCTSingleSidedBorder alloc] initWithSide:side superlayer:self.layer]; - [borders setObject:border forKey:@(side)]; - } - return border; -} - -@end