Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS: Support corner-specific border radii on Images #14148

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Libraries/Image/RCTImageView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
99 changes: 99 additions & 0 deletions Libraries/Image/RCTImageView.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#import "RCTImageView.h"

#import <React/RCTBorderDrawing.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTEventDispatcher.h>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]) {
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could you remove the extra empty lines from L493 to L502


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];
Expand All @@ -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
9 changes: 0 additions & 9 deletions docs/Images.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.