feat: linear gradient ios (#45434)

Summary:
- Adds `background` prop that supports CSS's linear gradient. Later this can be extended to support various other gradients and possibly CSS's background image (less motivation as better solutions exists for image)
- Uses `CAGradientlayer` to draw Linear Gradient layers. So it is GPU optimised under the hood.
- Style supports JS object to specify `LinearGradient`, so it can support Animated libraries.

## Changelog:
[IOS] [ADDED] - linear gradient

<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests

Pull Request resolved: https://github.com/facebook/react-native/pull/45434

Test Plan:
- Check out `processBackground-test.js` for supported syntax testcases.
- Checkout example added in ViewExample.js

Although the PR is tested well but open to any changes/feedback on the approach taken.

Android PR - https://github.com/facebook/react-native/pull/45433. Separated the PRs to keep it easier to review. Both PRs can be reviewed individually.

Reviewed By: NickGerleman

Differential Revision: D60791581

Pulled By: joevilches

fbshipit-source-id: 051088fdf68d9fe20c0c306f1f1c591cbd77f3d5
This commit is contained in:
Nishan
2024-08-06 13:59:33 -07:00
committed by Facebook GitHub Bot
parent 09fd7528b6
commit b99675d78a
3 changed files with 103 additions and 0 deletions
@@ -33,6 +33,7 @@ using namespace facebook::react;
__weak CALayer *_borderLayer;
CALayer *_boxShadowLayer;
CALayer *_filterLayer;
NSMutableArray<CAGradientLayer *> *_gradientLayers;
BOOL _needsInvalidateLayer;
BOOL _isJSResponder;
BOOL _removeClippedSubviews;
@@ -458,6 +459,11 @@ using namespace facebook::react;
}
}
// `linearGradient`
if (oldViewProps.backgroundImage != newViewProps.backgroundImage) {
needsInvalidateLayer = YES;
}
// `boxShadow`
if (oldViewProps.boxShadow != newViewProps.boxShadow) {
needsInvalidateLayer = YES;
@@ -830,6 +836,56 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle)
[self.layer addSublayer:_filterLayer];
}
[self clearExistingGradientLayers];
auto backgroundImage = _props->backgroundImage;
if (backgroundImage.empty()) {
return;
}
for (const auto &gradient : backgroundImage) {
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
NSMutableArray *colors = [NSMutableArray array];
NSMutableArray *locations = [NSMutableArray array];
for (const auto &colorStop : gradient.colorStops) {
if (colorStop.position.has_value()) {
auto location = @(colorStop.position.value());
UIColor *color = RCTUIColorFromSharedColor(colorStop.color);
[colors addObject:(id)color.CGColor];
[locations addObject:location];
}
}
gradientLayer.startPoint = CGPointMake(gradient.startX, gradient.startY);
gradientLayer.endPoint = CGPointMake(gradient.endX, gradient.endY);
if (locations.count > 0) {
gradientLayer.locations = locations;
}
gradientLayer.colors = colors;
gradientLayer.frame = layer.bounds;
// border styling to work with gradient layers
if (useCoreAnimationBorderRendering) {
gradientLayer.borderWidth = layer.borderWidth;
gradientLayer.borderColor = layer.borderColor;
gradientLayer.cornerRadius = layer.cornerRadius;
gradientLayer.cornerCurve = layer.cornerCurve;
} else {
CAShapeLayer *maskLayer = [CAShapeLayer layer];
CGPathRef path = RCTPathCreateWithRoundedRect(
self.bounds,
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero),
nil);
maskLayer.path = path;
CGPathRelease(path);
gradientLayer.mask = maskLayer;
}
// border layer should appear above gradient layers to make sure that the border is visible
gradientLayer.zPosition = _borderLayer.zPosition - 1;
[self.layer addSublayer:gradientLayer];
[_gradientLayers addObject:gradientLayer];
}
[_boxShadowLayer removeFromSuperlayer];
_boxShadowLayer = nil;
if (!_props->boxShadow.empty()) {
@@ -848,6 +904,18 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle)
}
}
- (void)clearExistingGradientLayers
{
if (_gradientLayers == nil) {
_gradientLayers = [NSMutableArray new];
return;
}
for (CAGradientLayer *gradientLayer in _gradientLayers) {
[gradientLayer removeFromSuperlayer];
}
[_gradientLayers removeAllObjects];
}
#pragma mark - Accessibility
- (NSObject *)accessibilityElement
@@ -126,4 +126,35 @@ exports.examples = [
);
},
},
{
title: 'Gradient with uniform border style',
render(): React.Node {
return (
<GradientBox
testID="linear-gradient-with-uniform-borders"
style={{
experimental_backgroundImage:
'linear-gradient(to bottom right, yellow, green);',
borderRadius: 16,
}}
/>
);
},
},
{
title: 'Gradient with non-uniform border style',
render(): React.Node {
return (
<GradientBox
testID="linear-gradient-with-non-uniform-borders"
style={{
experimental_backgroundImage:
'linear-gradient(to bottom right, yellow, green);',
borderTopRightRadius: 8,
borderTopLeftRadius: 80,
}}
/>
);
},
},
];
@@ -298,6 +298,10 @@ const APIs: Array<RNTesterModuleInfo> = ([
key: 'FilterExample',
module: require('../examples/Filter/FilterExample'),
},
{
key: 'LinearGradient',
module: require('../examples/LinearGradient/LinearGradientExample'),
},
{
key: 'MixBlendModeExample',
module: require('../examples/MixBlendMode/MixBlendModeExample'),