diff --git a/index.android.js b/index.android.js index 63d59811..fef10c4e 100644 --- a/index.android.js +++ b/index.android.js @@ -85,7 +85,8 @@ export default class LinearGradient extends Component { { + startPoint?: { x: number; y: number }, + endPoint?: { x: number; y: number }, + } + export class LinearGradient extends React.Component {} + export class LinearGradientNativeComponent extends React.Component {} export default LinearGradient; } diff --git a/index.ios.js b/index.ios.js index 489f2146..2e153519 100644 --- a/index.ios.js +++ b/index.ios.js @@ -56,7 +56,7 @@ export default class LinearGradient extends Component { {...otherProps} startPoint={convertPoint('start', start)} endPoint={convertPoint('end', end)} - colors={colors.map(processColor)} + colors={global.RN$Bridgeless ? colors : colors.map(processColor)} locations={locations ? locations.slice(0, colors.length) : null} useAngle={useAngle} angleCenter={convertPoint('angleCenter', angleCenter)} diff --git a/index.js b/index.js index d0403550..d6721d37 100644 --- a/index.js +++ b/index.js @@ -2,9 +2,12 @@ import { Platform } from "react-native"; import LinearGradientIos from "./index.ios.js"; import LinearGradientAndroid from "./index.android.js"; import LinearGradientWindows from "./index.windows.js"; +import LinearGradientNativeComponentRN from "./src/index.js"; export const LinearGradient = Platform.OS === "ios" ? LinearGradientIos : Platform.OS === "android" ? LinearGradientAndroid : LinearGradientWindows; +export const LinearGradientNativeComponent = LinearGradientNativeComponentRN; + export default LinearGradient; diff --git a/ios/RNLinearGradient.mm b/ios/RNLinearGradient.mm index 509fc9bd..c181bd4e 100644 --- a/ios/RNLinearGradient.mm +++ b/ios/RNLinearGradient.mm @@ -8,7 +8,9 @@ #import #import -#import "RCTFabricComponentsPlugins.h" +#import + +#import "RNLinearGradientLayerNewArch.h" #endif #import @@ -29,13 +31,24 @@ @implementation RNLinearGradient + (Class)layerClass { +#ifdef RCT_NEW_ARCH_ENABLED + return [RNLinearGradientLayerNewArch class]; +#else return [RNLinearGradientLayer class]; +#endif } +#ifdef RCT_NEW_ARCH_ENABLED +- (RNLinearGradientLayerNewArch *)gradientLayer +{ + return (RNLinearGradientLayerNewArch *)self.layer; +} +#else - (RNLinearGradientLayer *)gradientLayer { return (RNLinearGradientLayer *)self.layer; } +#endif - (NSArray *)colors { @@ -161,13 +174,15 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & self.angleCenter = CGPointMake(newViewProps.angleCenter.x, newViewProps.angleCenter.y); } - NSArray *locations = convertCxxVectorNumberToNsArrayNumber(newViewProps.locations); - self.locations = locations; + if (oldViewProps.locations != newViewProps.locations) { + NSArray *locations = convertCxxVectorNumberToNsArrayNumber(newViewProps.locations); + self.locations = locations; + } - // We cannot compare SharedColor because it is shared value. - // We could compare color value, but it is more performant to just assign new value - NSArray *colors = convertCxxVectorColorsToNSArrayColors(newViewProps.colors); - self.colors = colors; + if (oldViewProps.colors != newViewProps.colors) { + NSArray *colors = convertCxxVectorColorsToNSArrayColors(newViewProps.colors); + self.colors = colors; + } [super updateProps:props oldProps:oldProps]; } @@ -175,7 +190,6 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & static NSArray *convertCxxVectorColorsToNSArrayColors(const std::vector &colors) { size_t size = colors.size(); - NSLog(@"%zu", size); NSMutableArray *result = [NSMutableArray new]; for(size_t i = 0; i < size; i++) { UIColor *color = RCTUIColorFromSharedColor(colors[i]); diff --git a/ios/RNLinearGradientLayer.m b/ios/RNLinearGradientLayer.m index a2ff8799..4ea0ce9c 100644 --- a/ios/RNLinearGradientLayer.m +++ b/ios/RNLinearGradientLayer.m @@ -13,10 +13,11 @@ - (instancetype)init { self.needsDisplayOnBoundsChange = YES; self.masksToBounds = YES; - _startPoint = CGPointMake(0.5, 0.0); - _endPoint = CGPointMake(0.5, 1.0); - _angleCenter = CGPointMake(0.5, 0.5); - _angle = 45.0; + _startPoint = CGPointMake(0.0, 0.0); + _endPoint = CGPointMake(0.0, 0.0); + _angleCenter = CGPointMake(0.0, 0.0); + _angle = 0.0; + _useAngle = false; } return self; diff --git a/ios/RNLinearGradientLayerNewArch.h b/ios/RNLinearGradientLayerNewArch.h new file mode 100644 index 00000000..616c3ccf --- /dev/null +++ b/ios/RNLinearGradientLayerNewArch.h @@ -0,0 +1,16 @@ +#import +#import + +@class UIColor; + +@interface RNLinearGradientLayerNewArch : CALayer + +@property(nullable, nonatomic, copy) NSArray *colors; +@property(nullable, nonatomic, copy) NSArray *locations; +@property(nonatomic) CGPoint startPoint; +@property(nonatomic) CGPoint endPoint; +@property(nonatomic) BOOL useAngle; +@property(nonatomic) CGPoint angleCenter; +@property(nonatomic) CGFloat angle; + +@end diff --git a/ios/RNLinearGradientLayerNewArch.m b/ios/RNLinearGradientLayerNewArch.m new file mode 100644 index 00000000..7f5f035a --- /dev/null +++ b/ios/RNLinearGradientLayerNewArch.m @@ -0,0 +1,229 @@ +#import "RNLinearGradientLayerNewArch.h" + +#include +#import + +@implementation RNLinearGradientLayerNewArch +{ + CALayer *_clipLayer; + CAGradientLayer *_gradientLayer; +} + +- (instancetype)init +{ + self = [super init]; + + if (self) + { + // _clipLayer clips the gradient to the outer bounds + cornerRadius, + // independently of self.masksToBounds (which follows yoga overflow). + _clipLayer = [CALayer layer]; + _clipLayer.masksToBounds = YES; + // Push the gradient behind RN-child sublayers (default zPosition 0) + // but in front of RCTView's private background / border / shadow + // sublayers, which Fabric's RCTViewComponentView pins to + // BACKGROUND_COLOR_ZPOSITION = -1024. -CGFLOAT_MAX would sit behind + // those too, hiding the gradient whenever a consumer sets + // `backgroundColor` or `boxShadow` on . -512 picks a + // finite, comfortable midpoint. + _clipLayer.zPosition = -512.0; + [self addSublayer:_clipLayer]; + + _gradientLayer = [CAGradientLayer layer]; + // Our geometry math assumes rotation pivots around the gradient + // layer's center. CALayer's default anchorPoint is (0.5, 0.5); set + // it explicitly so a future subclass / animator can't silently move + // the pivot. + _gradientLayer.anchorPoint = CGPointMake(0.5, 0.5); + [_clipLayer addSublayer:_gradientLayer]; + + #ifndef RCT_NEW_ARCH_ENABLED + self.masksToBounds = YES; + #endif + _startPoint = CGPointMake(0.0, 0.0); + _endPoint = CGPointMake(0.0, 0.0); + _angleCenter = CGPointMake(0.0, 0.0); + _angle = 0.0; + _useAngle = NO; + } + + return self; +} + +- (void)layoutSublayers +{ + [super layoutSublayers]; + + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + + _clipLayer.frame = self.bounds; + [self mirrorCornerShape]; + [self applyGradientGeometry]; + + [CATransaction commit]; +} + +- (void)setCornerRadius:(CGFloat)cornerRadius +{ + [super setCornerRadius:cornerRadius]; + // RCTView updates cornerRadius without changing bounds, so layoutSublayers + // won't fire — mirror it onto _clipLayer immediately or the gradient clip + // lags behind the outer view's rounded corners. + [self mirrorCornerShape]; +} + +- (void)mirrorCornerShape +{ + if (!_clipLayer) return; + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + _clipLayer.cornerRadius = fmax(0.0, self.cornerRadius); + if (@available(iOS 13.0, *)) { + _clipLayer.cornerCurve = self.cornerCurve; + } + [CATransaction commit]; +} + +- (void)setColors:(NSArray *)colors +{ + _colors = [colors copy]; + + NSMutableArray *cgColors = [NSMutableArray arrayWithCapacity:colors.count]; + for (UIColor *color in colors) { + [cgColors addObject:(id)color.CGColor]; + } + + // _gradientLayer is a standalone sublayer (not a UIView's backing layer), + // so CAGradientLayer's animatable `colors` uses its default implicit + // action and crossfades on every assignment. Disable actions so JS prop + // updates snap. + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + _gradientLayer.colors = cgColors; + [CATransaction commit]; +} + +- (void)setLocations:(NSArray *)locations +{ + _locations = [locations copy]; + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + _gradientLayer.locations = locations; + [CATransaction commit]; +} + +- (void)setStartPoint:(CGPoint)startPoint +{ + _startPoint = startPoint; + [self applyGradientGeometry]; +} + +- (void)setEndPoint:(CGPoint)endPoint +{ + _endPoint = endPoint; + [self applyGradientGeometry]; +} + +- (void)setUseAngle:(BOOL)useAngle +{ + _useAngle = useAngle; + [self applyGradientGeometry]; +} + +- (void)setAngleCenter:(CGPoint)angleCenter +{ + _angleCenter = angleCenter; + [self applyGradientGeometry]; +} + +- (void)setAngle:(CGFloat)angle +{ + _angle = angle; + [self applyGradientGeometry]; +} + +- (void)applyGradientGeometry +{ + CGSize size = self.bounds.size; + CGFloat W = size.width; + CGFloat H = size.height; + + // Bail if bounds aren't set yet — setters can fire before the first + // layout pass. layoutSublayers will re-run geometry once W/H are real. + if (W <= 0.0 || H <= 0.0) { + return; + } + + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + + if (!_useAngle) { + _gradientLayer.bounds = CGRectMake(0, 0, W, H); + _gradientLayer.position = CGPointMake(W / 2.0, H / 2.0); + _gradientLayer.transform = CATransform3DIdentity; + _gradientLayer.startPoint = _startPoint; + _gradientLayer.endPoint = _endPoint; + } else { + // Guard numerics: NaN propagates into every CA property and wipes + // the gradient; large angles lose sin/cos precision via argument + // reduction. + CGFloat angle = isfinite(_angle) ? fmod(_angle, 360.0) : 0.0; + CGFloat acxNorm = isfinite(_angleCenter.x) ? _angleCenter.x : 0.5; + CGFloat acyNorm = isfinite(_angleCenter.y) ? _angleCenter.y : 0.5; + + // Render a bearing-0 (pointing-up / north) gradient in a square + // sublayer and rotate it into place, rather than letting + // CAGradientLayer handle the angle directly. CAGradientLayer's + // iso-color lines are perpendicular to startPoint→endPoint in + // unit space, which on a non-square layer gets stretched under the + // unit→pixel mapping and compresses the visible t-range. A square + // layer has W = H, so perpendicular-in-unit-space stays + // perpendicular-in-pixel-space after the rotation transform. + CGFloat bearingRad = angle * M_PI / 180.0; + // Half the projection of the parent rectangle onto the bearing + // direction — i.e., the CSS-spec distance from the rectangle's + // center to the 0% / 100% stop along the gradient line. Note this + // uses the rectangle's geometric center, not angleCenter, matching + // the CSS `linear-gradient` spec (which has no angleCenter concept). + CGFloat halfExtent = (W * fabs(sin(bearingRad)) + H * fabs(cos(bearingRad))) / 2.0; + + // The square must cover every parent corner in the rotated + // frame. A corner at (cx, cy) relative to angleCenter projects + // to (x', y') on the rotated axes; we need both |x'| and |y'| + // ≤ layerSize/2. Taking the max over all four corners of + // max(|x'|, |y'|) and doubling gives the tight square size. + CGFloat acx = acxNorm * W; + CGFloat acy = acyNorm * H; + CGFloat cosA = cos(bearingRad); + CGFloat sinA = sin(bearingRad); + CGFloat cornerDx[4] = { -acx, W - acx, -acx, W - acx }; + CGFloat cornerDy[4] = { -acy, -acy, H - acy, H - acy }; + CGFloat maxHalf = 0.0; + for (int i = 0; i < 4; i++) { + CGFloat xp = cornerDx[i] * cosA + cornerDy[i] * sinA; + CGFloat yp = -cornerDx[i] * sinA + cornerDy[i] * cosA; + maxHalf = fmax(maxHalf, fmax(fabs(xp), fabs(yp))); + } + CGFloat layerSize = 2.0 * maxHalf + 2.0; + + // Pin startPoint / endPoint to ±halfExtent pixels from the layer's + // center regardless of layerSize. That nails the 0% and 100% stops + // to the CSS-spec pixel offset from angleCenter; the layer itself + // is free to extend past the parent for full coverage. + // startPoint is the 0% stop (below center), endPoint the 100% stop + // (above center). Rotating a north-pointing gradient by `_angle` + // degrees clockwise produces the bearing direction directly — no + // offset needed. + CGFloat ratio = halfExtent / layerSize; + _gradientLayer.bounds = CGRectMake(0, 0, layerSize, layerSize); + _gradientLayer.position = CGPointMake(acx, acy); + _gradientLayer.startPoint = CGPointMake(0.5, 0.5 + ratio); + _gradientLayer.endPoint = CGPointMake(0.5, 0.5 - ratio); + _gradientLayer.transform = CATransform3DMakeRotation(bearingRad, 0, 0, 1); + } + + [CATransaction commit]; +} + +@end diff --git a/package.json b/package.json index 9806319d..d2bc0f17 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,11 @@ "codegenConfig": { "name": "RNLinearGradientSpec", "type": "components", - "jsSrcsDir": "src" + "jsSrcsDir": "src", + "ios": { + "componentProvider": { + "RNLinearGradient": "RNLinearGradient" + } + } } }