diff --git a/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm b/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm index e9ce272b5f55fb..6e88d6f922799a 100644 --- a/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm +++ b/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm @@ -28,6 +28,7 @@ - (RCTShadowView *)shadowView // Color RCT_REMAP_SHADOW_PROPERTY(color, textAttributes.foregroundColor, UIColor) RCT_REMAP_SHADOW_PROPERTY(backgroundColor, textAttributes.backgroundColor, UIColor) +RCT_REMAP_SHADOW_PROPERTY(gradientColors, textAttributes.gradientColors, NSArray) RCT_REMAP_SHADOW_PROPERTY(opacity, textAttributes.opacity, CGFloat) // Font RCT_REMAP_SHADOW_PROPERTY(fontFamily, textAttributes.fontFamily, NSString) diff --git a/packages/react-native/Libraries/Text/RCTTextAttributes.h b/packages/react-native/Libraries/Text/RCTTextAttributes.h index c6923928d2d224..c656d369c16fc4 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.h +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.h @@ -27,6 +27,7 @@ extern NSString *const RCTTextAttributesTagAttributeName; // Color @property (nonatomic, strong, nullable) UIColor *foregroundColor; @property (nonatomic, strong, nullable) UIColor *backgroundColor; +@property (nonatomic, copy, nullable) NSArray *gradientColors; @property (nonatomic, assign) CGFloat opacity; // Font @property (nonatomic, copy, nullable) NSString *fontFamily; diff --git a/packages/react-native/Libraries/Text/RCTTextAttributes.mm b/packages/react-native/Libraries/Text/RCTTextAttributes.mm index 2462390c95d8a2..604ef0fc88bae0 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.mm +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.mm @@ -46,6 +46,7 @@ - (void)applyTextAttributes:(RCTTextAttributes *)textAttributes // Color _foregroundColor = textAttributes->_foregroundColor ?: _foregroundColor; _backgroundColor = textAttributes->_backgroundColor ?: _backgroundColor; + _gradientColors = textAttributes->_gradientColors ?: _gradientColors; _opacity = !isnan(textAttributes->_opacity) ? (isnan(_opacity) ? 1.0 : _opacity) * textAttributes->_opacity : _opacity; @@ -294,6 +295,34 @@ - (UIColor *)effectiveForegroundColor { UIColor *effectiveForegroundColor = _foregroundColor ?: [UIColor blackColor]; + if (_gradientColors != nil) { + NSMutableArray *cgColors = [NSMutableArray array]; + for (NSNumber *rawColor in _gradientColors) { + if (rawColor != nil) { + UIColor *color = [RCTConvert UIColor:@((0xFF << 24) | [rawColor integerValue])]; + [cgColors addObject:(id)color.CGColor]; + } + } + + if([cgColors count] > 0) { + CAGradientLayer *gradient = [CAGradientLayer layer]; + // this pattern width corresponds roughly to desktop's pattern width + int patternWidth = 100; + CGFloat height = _lineHeight * self.effectiveFontSizeMultiplier; + gradient.frame = CGRectMake(0, 0, patternWidth, height); + gradient.colors = cgColors; + gradient.startPoint = CGPointMake(0.0, 0.5); + gradient.endPoint = CGPointMake(1.0, 0.5); + + UIGraphicsBeginImageContextWithOptions(gradient.frame.size, NO, 0.0); + [gradient renderInContext:UIGraphicsGetCurrentContext()]; + UIImage *gradientImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + effectiveForegroundColor = [UIColor colorWithPatternImage:gradientImage]; + } + } + if (!isnan(_opacity)) { effectiveForegroundColor = [effectiveForegroundColor colorWithAlphaComponent:CGColorGetAlpha(effectiveForegroundColor.CGColor) * _opacity]; diff --git a/packages/react-native/Libraries/Text/Text.d.ts b/packages/react-native/Libraries/Text/Text.d.ts index b90bf240ba72d9..20a84306733fd8 100644 --- a/packages/react-native/Libraries/Text/Text.d.ts +++ b/packages/react-native/Libraries/Text/Text.d.ts @@ -209,6 +209,11 @@ export interface TextProps * Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0). */ minimumFontScale?: number | undefined; + + /** + * Adds a horizontal gradient using the int based color values. + */ + gradientColors?: number[] | undefined; } /** diff --git a/packages/react-native/Libraries/Text/TextNativeComponent.js b/packages/react-native/Libraries/Text/TextNativeComponent.js index 90f9e91b646d54..8dc081e3b6f958 100644 --- a/packages/react-native/Libraries/Text/TextNativeComponent.js +++ b/packages/react-native/Libraries/Text/TextNativeComponent.js @@ -49,6 +49,7 @@ const textViewConfig = { dataDetectorType: true, android_hyphenationFrequency: true, lineBreakStrategyIOS: true, + gradientColors: true, }, directEventTypes: { topTextLayout: { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index 6cb16d00028de2..8a6eb83930da07 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -20,6 +20,7 @@ import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.IllegalViewOperationException; import com.facebook.react.uimanager.LayoutShadowNode; @@ -34,6 +35,7 @@ import com.facebook.react.views.text.internal.span.CustomLetterSpacingSpan; import com.facebook.react.views.text.internal.span.CustomLineHeightSpan; import com.facebook.react.views.text.internal.span.CustomStyleSpan; +import com.facebook.react.views.text.internal.span.LinearGradientSpan; import com.facebook.react.views.text.internal.span.ReactAbsoluteSizeSpan; import com.facebook.react.views.text.internal.span.ReactBackgroundColorSpan; import com.facebook.react.views.text.internal.span.ReactClickableSpan; @@ -160,9 +162,15 @@ private static void buildSpannedFromShadowNode( } int end = sb.length(); if (end >= start) { - if (textShadowNode.mIsColorSet) { - ops.add( - new SetSpanOperation(start, end, new ReactForegroundColorSpan(textShadowNode.mColor))); + if (textShadowNode.mIsColorSet || textShadowNode.mGradientColors != null) { + if (textShadowNode.mGradientColors != null && textShadowNode.mGradientColors.length >= 2) { + int effectiveFontSize = textAttributes.getEffectiveFontSize(); + ops.add( + new SetSpanOperation(start, end, new LinearGradientSpan(start * effectiveFontSize, textShadowNode.mGradientColors))); + } else { + ops.add( + new SetSpanOperation(start, end, new ReactForegroundColorSpan(textShadowNode.mColor))); + } } if (textShadowNode.mIsBackgroundColorSet) { ops.add( @@ -319,6 +327,8 @@ protected Spannable spannedFromShadowNode( protected boolean mIsBackgroundColorSet = false; protected int mBackgroundColor; + protected @Nullable int[] mGradientColors = null; + protected @Nullable AccessibilityRole mAccessibilityRole = null; protected @Nullable Role mRole = null; @@ -479,6 +489,30 @@ public void setColor(@Nullable Integer color) { markUpdated(); } + @ReactProp(name = "gradientColors") + public void setGradientColors(@Nullable ReadableArray gradientColors) { + if (gradientColors != null) { + ArrayList colors = new ArrayList(); + + for (int i = 0; i < gradientColors.size(); i++) { + if (!gradientColors.isNull(i) && gradientColors.getType(i) == ReadableType.Number) { + int color = gradientColors.getInt(i); + colors.add(color); + } + } + + int colorsSize = colors.size(); + if (colorsSize >= 2) { + int[] colorsAsList = new int[colorsSize]; + for (int i = 0; i < colorsSize; i++) { + colorsAsList[i] = colors.get(i); + } + + mGradientColors = colorsAsList; + } + } + } + @ReactProp(name = ViewProps.BACKGROUND_COLOR, customType = "Color") public void setBackgroundColor(@Nullable Integer color) { // Background color needs to be handled here for virtual nodes so it can be incorporated into diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/LinearGradientSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/LinearGradientSpan.kt new file mode 100644 index 00000000000000..76a6d88e0be01c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/LinearGradientSpan.kt @@ -0,0 +1,27 @@ +package com.facebook.react.views.text.internal.span + +import android.graphics.LinearGradient +import android.graphics.Shader +import android.text.TextPaint +import android.text.style.CharacterStyle +import android.text.style.UpdateAppearance + +public class LinearGradientSpan( + private val start: Float, + private val colors: IntArray, +) : CharacterStyle(), ReactSpan, + UpdateAppearance { + public override fun updateDrawState(tp: TextPaint) { + val textShader: Shader = + LinearGradient( + start, + 0f, + start + 150.0f, + 0f, + colors, + null, + Shader.TileMode.MIRROR, + ) + tp.setShader(textShader) + } +}