diff --git a/Libraries/Image/RCTImageView.h b/Libraries/Image/RCTImageView.h index 7c632b3bfa66a1..6210963f0927b7 100644 --- a/Libraries/Image/RCTImageView.h +++ b/Libraries/Image/RCTImageView.h @@ -25,4 +25,13 @@ @property (nonatomic, assign) CGFloat blurRadius; @property (nonatomic, assign) RCTResizeMode resizeMode; +/** + * Border radii. + */ +@property (nonatomic, assign) CGFloat borderRadius; +@property (nonatomic, assign) CGFloat borderTopLeftRadius; +@property (nonatomic, assign) CGFloat borderTopRightRadius; +@property (nonatomic, assign) CGFloat borderBottomLeftRadius; +@property (nonatomic, assign) CGFloat borderBottomRightRadius; + @end diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 6387987e699e50..a8f211180df584 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -9,6 +9,7 @@ #import "RCTImageView.h" +#import #import #import #import @@ -87,6 +88,11 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge if ((self = [super init])) { _bridge = bridge; + _borderTopLeftRadius = -1; + _borderTopRightRadius = -1; + _borderBottomLeftRadius = -1; + _borderBottomRightRadius = -1; + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(clearImageIfDetached) @@ -131,6 +137,10 @@ - (void)updateWithImage:(UIImage *)image self.layer.magnificationFilter = kCAFilterTrilinear; super.image = image; + + if (self.hasBorderRadii) { + [self.layer setNeedsDisplay]; + } } - (void)setImage:(UIImage *)image @@ -326,6 +336,10 @@ - (void)reloadImage } else { [self clearImage]; } + + if (self.hasBorderRadii) { + [self.layer setNeedsDisplay]; + } } - (void)imageLoaderLoadedImage:(UIImage *)loadedImage error:(NSError *)error forImageSource:(RCTImageSource *)source partial:(BOOL)isPartialLoad @@ -393,8 +407,15 @@ - (void)imageLoaderLoadedImage:(UIImage *)loadedImage error:(NSError *)error for - (void)reactSetFrame:(CGRect)frame { + CGSize oldSize = self.bounds.size; [super reactSetFrame:frame]; + // If the size changed and we have border radius values, we may need to + // recompute the border radius mask based on the new size. + if (!CGSizeEqualToSize(self.bounds.size, oldSize) && self.hasBorderRadii) { + [self.layer setNeedsDisplay]; + } + // If we didn't load an image yet, or the new frame triggers a different image source // to be loaded, reload to swap to the proper image source. if ([self shouldChangeImageSource]) { @@ -428,6 +449,68 @@ - (void)reactSetFrame:(CGRect)frame } } +- (BOOL)hasBorderRadii +{ + return _borderRadius > 0 || _borderTopLeftRadius > 0 || _borderTopRightRadius > 0 || + _borderBottomLeftRadius > 0 || _borderBottomRightRadius; +} + +- (RCTCornerRadii)cornerRadii +{ + // Get corner radii + const CGFloat radius = MAX(0, _borderRadius); + const CGFloat topLeftRadius = _borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius; + const CGFloat topRightRadius = _borderTopRightRadius >= 0 ? _borderTopRightRadius : radius; + const CGFloat bottomLeftRadius = _borderBottomLeftRadius >= 0 ? _borderBottomLeftRadius : radius; + const CGFloat bottomRightRadius = _borderBottomRightRadius >= 0 ? _borderBottomRightRadius : radius; + + // Get scale factors required to prevent radii from overlapping + const CGSize size = self.bounds.size; + const CGFloat topScaleFactor = RCTZeroIfNaN(MIN(1, size.width / (topLeftRadius + topRightRadius))); + const CGFloat bottomScaleFactor = RCTZeroIfNaN(MIN(1, size.width / (bottomLeftRadius + bottomRightRadius))); + const CGFloat rightScaleFactor = RCTZeroIfNaN(MIN(1, size.height / (topRightRadius + bottomRightRadius))); + const CGFloat leftScaleFactor = RCTZeroIfNaN(MIN(1, size.height / (topLeftRadius + bottomLeftRadius))); + + // Return scaled radii + return (RCTCornerRadii){ + topLeftRadius * MIN(topScaleFactor, leftScaleFactor), + topRightRadius * MIN(topScaleFactor, rightScaleFactor), + bottomLeftRadius * MIN(bottomScaleFactor, leftScaleFactor), + bottomRightRadius * MIN(bottomScaleFactor, rightScaleFactor), + }; +} + +- (void)displayLayer:(CALayer *)layer +{ + [self updateClippingForLayer:layer]; +} + +- (void)updateClippingForLayer:(CALayer *)layer +{ + CALayer *mask = nil; + CGFloat cornerRadius = 0; + + if (self.clipsToBounds) { + + const RCTCornerRadii cornerRadii = [self cornerRadii]; + if (RCTCornerRadiiAreEqual(cornerRadii)) { + + cornerRadius = cornerRadii.topLeft; + + } else { + + CAShapeLayer *shapeLayer = [CAShapeLayer layer]; + CGPathRef path = RCTPathCreateWithRoundedRect(self.bounds, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL); + shapeLayer.path = path; + CGPathRelease(path); + mask = shapeLayer; + } + } + + layer.cornerRadius = cornerRadius; + layer.mask = mask; +} + - (void)didMoveToWindow { [super didMoveToWindow]; @@ -443,4 +526,20 @@ - (void)didMoveToWindow } } +#define setBorderRadius(side) \ + - (void)setBorder##side##Radius:(CGFloat)radius \ + { \ + if (_border##side##Radius == radius) { \ + return; \ + } \ + _border##side##Radius = radius; \ + [self.layer setNeedsDisplay]; \ + } + +setBorderRadius() +setBorderRadius(TopLeft) +setBorderRadius(TopRight) +setBorderRadius(BottomLeft) +setBorderRadius(BottomRight) + @end diff --git a/docs/Images.md b/docs/Images.md index e1dc0ee7b3e6e9..1fd9a0b7b5e90c 100644 --- a/docs/Images.md +++ b/docs/Images.md @@ -176,15 +176,6 @@ return ( ); ``` -## iOS Border Radius Styles - -Please note that the following corner specific, border radius style properties are currently ignored by iOS's image component: - -* `borderTopLeftRadius` -* `borderTopRightRadius` -* `borderBottomLeftRadius` -* `borderBottomRightRadius` - ## Off-thread Decoding Image decoding can take more than a frame-worth of time. This is one of the major sources of frame drops on the web because decoding is done in the main thread. In React Native, image decoding is done in a different thread. In practice, you already need to handle the case when the image is not downloaded yet, so displaying the placeholder for a few more frames while it is decoding does not require any code change.