diff --git a/packages/react-native/React/Views/RCTBorderDrawing.m b/packages/react-native/React/Views/RCTBorderDrawing.m index 472885ca0c39..d13327752da6 100644 --- a/packages/react-native/React/Views/RCTBorderDrawing.m +++ b/packages/react-native/React/Views/RCTBorderDrawing.m @@ -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 *RCTSolidBorderImageCache(void) +{ + static NSCache *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [NSCache new]; + cache.countLimit = 64; + }); + return cache; +} + static UIImage *RCTGetSolidBorderImage( RCTCornerRadii cornerRadii, CGSize viewSize, @@ -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. @@ -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), @@ -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;