Skip to content

Commit 097d482

Browse files
fix(ios): Correct gradient interpolation for when transitioning to transparent color (#52249)
Summary: This change fixes an issue on iOS where gradients that fade to a transparent color-stop appear dark or "muddy." The fix ensures that the color's hue is preserved during the transition, matching the behavior on Android and web. ### The Problem When creating a gradient on iOS (e.g., linear-gradient(red, transparent)), the transparent keyword is treated as transparent black (rgba(0,0,0,0)). The `CAGradientLayer` on iOS then interpolates all color channels linearly, causing the red, green, and blue components of the start color to fade to 0. This transition through black results in an undesirable dark or "muddy" appearance in the middle of the gradient. ## Changelog: [IOS][FIXED] - Gradient interpolation for transparent colors <!-- 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: #52249 Test Plan: Checkout `LinearGradient` example in RNTester, checkout the newly added transparent color transition example, it should render same on android and iOS. | Before | After | | --- | --- | | <img src="https://github.com/user-attachments/assets/c0bb54ad-ed0e-4a80-b37f-0458af0f1f77" width="300"> | <img src="https://github.com/user-attachments/assets/02da921a-bd0e-45c1-881c-cf6460d5ed43" width="300"> | | `linear-gradient(to right, red, transparent)` transitions to black on iOS, creating a dark effect. | The gradient correctly fades the red color's alpha channel to zero | Reviewed By: javache Differential Revision: D77312194 Pulled By: NickGerleman fbshipit-source-id: 053df8e44f52cd22a3f28fd01f583f7d03c66af5
1 parent 508b152 commit 097d482

5 files changed

Lines changed: 57 additions & 8 deletions

File tree

packages/react-native/React/Fabric/Utils/RCTGradientUtils.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ NS_ASSUME_NONNULL_BEGIN
2323
+ (std::pair<CGPoint, CGPoint>)pointsForCAGradientLayerLinearGradient:(CGPoint)startPoint
2424
endPoint:(CGPoint)endPoint
2525
bounds:(CGSize)bounds;
26+
27+
+ (void)getColors:(NSMutableArray<id> *)colors
28+
andLocations:(NSMutableArray<NSNumber *> *)locations
29+
fromColorStops:(const std::vector<facebook::react::ProcessedColorStop> &)colorStops;
30+
2631
@end
2732

2833
NS_ASSUME_NONNULL_END

packages/react-native/React/Fabric/Utils/RCTGradientUtils.mm

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,4 +374,38 @@ @implementation RCTGradientUtils
374374

375375
return {startPoint, endPoint};
376376
}
377+
+ (void)getColors:(NSMutableArray<id> *)colors
378+
andLocations:(NSMutableArray<NSNumber *> *)locations
379+
fromColorStops:(const std::vector<facebook::react::ProcessedColorStop> &)colorStops
380+
{
381+
// iOS's CAGradientLayer interpolates colors in a way that can cause unexpected results.
382+
// For example, a gradient from a color to `transparent` (which is transparent black) will
383+
// fade the color's RGB components to black, creating a "muddy" or dark appearance.
384+
// To fix this, we detect when a color stop is transparent black and replace it with
385+
// a transparent version of the *previous* color stop. This creates a smooth fade-out effect
386+
// by only interpolating the alpha channel, matching web and Android behavior.
387+
UIColor *lastColor = nil;
388+
for (const auto &colorStop : colorStops) {
389+
UIColor *currentColor = RCTUIColorFromSharedColor(colorStop.color);
390+
391+
CGFloat red = 0.0;
392+
CGFloat green = 0.0;
393+
CGFloat blue = 0.0;
394+
CGFloat alpha = 0.0;
395+
[currentColor getRed:&red green:&green blue:&blue alpha:&alpha];
396+
397+
BOOL isTransparentBlack = alpha == 0.0 && red == 0.0 && green == 0.0 && blue == 0.0;
398+
399+
if (isTransparentBlack && (lastColor != nullptr)) {
400+
[colors addObject:(id)[lastColor colorWithAlphaComponent:0.0].CGColor];
401+
} else {
402+
[colors addObject:(id)currentColor.CGColor];
403+
}
404+
405+
if (!isTransparentBlack) {
406+
lastColor = currentColor;
407+
}
408+
[locations addObject:@(std::max(std::min(colorStop.position.value(), 1.0), 0.0))];
409+
}
410+
}
377411
@end

packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,7 @@ + (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const LinearGradient &
5858
gradientLayer.startPoint = fixedStartPoint;
5959
gradientLayer.endPoint = fixedEndPoint;
6060

61-
for (const auto &colorStop : colorStops) {
62-
[colors addObject:(id)RCTUIColorFromSharedColor(colorStop.color).CGColor];
63-
[locations addObject:@(std::max(std::min(colorStop.position.value(), 1.0), 0.0))];
64-
}
61+
[RCTGradientUtils getColors:colors andLocations:locations fromColorStops:colorStops];
6562

6663
gradientLayer.colors = colors;
6764
gradientLayer.locations = locations;

packages/react-native/React/Fabric/Utils/RCTRadialGradient.mm

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,8 @@ + (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const RadialGradient &
182182

183183
NSMutableArray<id> *colors = [NSMutableArray array];
184184
NSMutableArray<NSNumber *> *locations = [NSMutableArray array];
185-
for (const auto &colorStop : colorStops) {
186-
[colors addObject:(id)RCTUIColorFromSharedColor(colorStop.color).CGColor];
187-
[locations addObject:@(std::max(std::min(colorStop.position.value(), 1.0), 0.0))];
188-
}
185+
186+
[RCTGradientUtils getColors:colors andLocations:locations fromColorStops:colorStops];
189187

190188
gradientLayer.colors = colors;
191189
gradientLayer.locations = locations;

packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,4 +290,19 @@ exports.examples = [
290290
);
291291
},
292292
},
293+
{
294+
title: 'Gradient with transparent color transition',
295+
name: 'transparent-color-transition',
296+
render(): React.Node {
297+
return (
298+
<GradientBox
299+
testID="linear-gradient-transparent-color-transition"
300+
style={{
301+
experimental_backgroundImage:
302+
'linear-gradient(to right, red, transparent)',
303+
}}
304+
/>
305+
);
306+
},
307+
},
293308
];

0 commit comments

Comments
 (0)