Skip to content
Open
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
126 changes: 126 additions & 0 deletions packages/react-native/React/Views/RCTBorderDrawing.m
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,119 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn
return renderer;
}

static id RCTSolidBorderImageCacheColor(UIColor *color)
{
if (color == nil) {
return [NSNull null];
}
// A dynamic color resolves differently per trait collection, so an image drawn with it can't be reused.
return [color resolvedColorWithTraitCollection:[UITraitCollection currentTraitCollection]] == color ? color : nil;
}

// Key of the solid border image cache. It hashes the radii and widths itself: NSArray hashes by count
// and NSValue gives every struct of a custom type the same hash.
@interface RCTSolidBorderImageCacheKey : NSObject
@end

@implementation RCTSolidBorderImageCacheKey {
RCTCornerRadii _cornerRadii;
UIEdgeInsets _borderInsets;
NSArray *_colors;
BOOL _drawToEdge;
NSUInteger _hash;
}

- (instancetype)initWithCornerRadii:(RCTCornerRadii)cornerRadii
borderInsets:(UIEdgeInsets)borderInsets
colors:(NSArray *)colors
drawToEdge:(BOOL)drawToEdge
{
if (self = [super init]) {
_cornerRadii = cornerRadii;
_borderInsets = borderInsets;
_colors = colors;
_drawToEdge = drawToEdge;
const CGFloat values[] = {
cornerRadii.topLeftHorizontal,
cornerRadii.topLeftVertical,
cornerRadii.topRightHorizontal,
cornerRadii.topRightVertical,
cornerRadii.bottomLeftHorizontal,
cornerRadii.bottomLeftVertical,
cornerRadii.bottomRightHorizontal,
cornerRadii.bottomRightVertical,
borderInsets.top,
borderInsets.left,
borderInsets.bottom,
borderInsets.right,
};
NSUInteger hash = drawToEdge;
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
uint64_t bits;
memcpy(&bits, &values[i], sizeof(bits));
hash = hash * 31 + (NSUInteger)(bits ^ (bits >> 32));
}
for (id color in colors) {
hash = hash * 31 + [color hash];
}
_hash = hash;
}
return self;
}

- (NSUInteger)hash
{
return _hash;
}

- (BOOL)isEqual:(id)object
{
if (![object isKindOfClass:[RCTSolidBorderImageCacheKey class]]) {
return NO;
}
RCTSolidBorderImageCacheKey *other = object;
return _hash == other->_hash && _drawToEdge == other->_drawToEdge &&
memcmp(&_cornerRadii, &other->_cornerRadii, sizeof(RCTCornerRadii)) == 0 &&
memcmp(&_borderInsets, &other->_borderInsets, sizeof(UIEdgeInsets)) == 0 &&
[_colors isEqualToArray:other->_colors];
}

@end

// A stretchable border image doesn't depend on the view size, only on these inputs, so views with the
// same border can share one image instead of each drawing its own.
static RCTSolidBorderImageCacheKey *RCTSolidBorderImageCacheKeyFor(
RCTCornerRadii cornerRadii,
UIEdgeInsets borderInsets,
RCTBorderColors borderColors,
UIColor *backgroundColor,
BOOL drawToEdge)
{
id top = RCTSolidBorderImageCacheColor(borderColors.top);
id left = RCTSolidBorderImageCacheColor(borderColors.left);
id bottom = RCTSolidBorderImageCacheColor(borderColors.bottom);
id right = RCTSolidBorderImageCacheColor(borderColors.right);
id background = RCTSolidBorderImageCacheColor(backgroundColor);
if (top == nil || left == nil || bottom == nil || right == nil || background == nil) {
return nil;
}
return [[RCTSolidBorderImageCacheKey alloc] initWithCornerRadii:cornerRadii
borderInsets:borderInsets
colors:@[ top, left, bottom, right, background ]
drawToEdge:drawToEdge];
}

static NSCache<RCTSolidBorderImageCacheKey *, UIImage *> *RCTSolidBorderImageCache(void)
{
static NSCache<RCTSolidBorderImageCacheKey *, UIImage *> *cache;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
cache = [NSCache new];
cache.countLimit = 64;
});
return cache;
}

static UIImage *RCTGetSolidBorderImage(
RCTCornerRadii cornerRadii,
CGSize viewSize,
Expand All @@ -223,6 +336,9 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn
{
const BOOL hasCornerRadii = RCTCornerRadiiAreAboveThreshold(cornerRadii);
const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, borderInsets);
// Keyed by the widths as passed in, since the corner insets above are computed from them before rounding.
RCTSolidBorderImageCacheKey *const cacheKey =
RCTSolidBorderImageCacheKeyFor(cornerRadii, borderInsets, borderColors, backgroundColor, drawToEdge);

// Incorrect render for borders that are not proportional to device pixel: borders get stretched and become
// significantly bigger than expected.
Expand All @@ -239,6 +355,13 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn
(borderInsets.top + cornerInsets.topRight.height + borderInsets.bottom + cornerInsets.bottomLeft.height <=
viewSize.height);

if (makeStretchable && cacheKey != nil) {
UIImage *cachedImage = [RCTSolidBorderImageCache() objectForKey:cacheKey];
if (cachedImage != nil) {
return cachedImage;
}
}

UIEdgeInsets edgeInsets =
(UIEdgeInsets){borderInsets.top + MAX(cornerInsets.topLeft.height, cornerInsets.topRight.height),
borderInsets.left + MAX(cornerInsets.topLeft.width, cornerInsets.bottomLeft.width),
Expand Down Expand Up @@ -417,6 +540,9 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn

if (makeStretchable) {
image = [image resizableImageWithCapInsets:edgeInsets];
if (cacheKey != nil) {
[RCTSolidBorderImageCache() setObject:image forKey:cacheKey];
}
}

return image;
Expand Down
Loading