diff --git a/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm b/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm index 67c4d674396e8a..ff69c3436e2093 100644 --- a/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm +++ b/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm @@ -30,6 +30,7 @@ - (RCTShadowView *)shadowView RCT_REMAP_SHADOW_PROPERTY(backgroundColor, textAttributes.backgroundColor, UIColor) RCT_REMAP_SHADOW_PROPERTY(gradientColors, textAttributes.gradientColors, NSArray) RCT_REMAP_SHADOW_PROPERTY(gradientAngle, textAttributes.gradientAngle, CGFloat) +RCT_REMAP_SHADOW_PROPERTY(gradientWidth, textAttributes.gradientWidth, CGFloat) 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 b087336ebd91d8..d246ec6a3f366e 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.h +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.h @@ -28,6 +28,7 @@ extern NSString *const RCTTextAttributesTagAttributeName; @property (nonatomic, strong, nullable) UIColor *backgroundColor; @property (nonatomic, copy, nullable) NSArray *gradientColors; @property (nonatomic, assign) CGFloat gradientAngle; +@property (nonatomic, assign) CGFloat gradientWidth; // Width of gradient pattern in pixels; NAN = use default (100px) @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 5494e15c374100..86fd380b2448bb 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.mm +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.mm @@ -50,6 +50,7 @@ - (void)applyTextAttributes:(RCTTextAttributes *)textAttributes _backgroundColor = textAttributes->_backgroundColor ?: _backgroundColor; _gradientColors = textAttributes->_gradientColors ?: _gradientColors; _gradientAngle = !isnan(textAttributes->_gradientAngle) ? textAttributes->_gradientAngle : _gradientAngle; + _gradientWidth = !isnan(textAttributes->_gradientWidth) ? textAttributes->_gradientWidth : _gradientWidth; _opacity = !isnan(textAttributes->_opacity) ? (isnan(_opacity) ? 1.0 : _opacity) * textAttributes->_opacity : _opacity; @@ -323,8 +324,8 @@ - (UIColor *)effectiveForegroundColor if([cgColors count] > 0) { [cgColors addObject:cgColors[0]]; CAGradientLayer *gradient = [CAGradientLayer layer]; - // this pattern width corresponds roughly to desktop's pattern width - int patternWidth = 100; + // Use gradientWidth if specified, otherwise default to 100 + CGFloat patternWidth = (!isnan(_gradientWidth) && _gradientWidth > 0) ? _gradientWidth : 100; CGFloat height = _lineHeight * self.effectiveFontSizeMultiplier; gradient.frame = CGRectMake(0, 0, patternWidth, height); gradient.colors = cgColors; @@ -423,7 +424,7 @@ - (BOOL)isEqual:(RCTTextAttributes *)textAttributes return RCTTextAttributesCompareObjects(_foregroundColor) && RCTTextAttributesCompareObjects(_backgroundColor) && RCTTextAttributesCompareObjects(_gradientColors) && RCTTextAttributesCompareFloats(_gradientAngle) && - RCTTextAttributesCompareFloats(_opacity) && + RCTTextAttributesCompareFloats(_gradientWidth) && RCTTextAttributesCompareFloats(_opacity) && // Font RCTTextAttributesCompareObjects(_fontFamily) && RCTTextAttributesCompareFloats(_fontSize) && RCTTextAttributesCompareFloats(_fontSizeMultiplier) && RCTTextAttributesCompareFloats(_maxFontSizeMultiplier) && diff --git a/packages/react-native/Libraries/Text/Text.d.ts b/packages/react-native/Libraries/Text/Text.d.ts index 06d0eef5e40f3c..fd8003817f41a3 100644 --- a/packages/react-native/Libraries/Text/Text.d.ts +++ b/packages/react-native/Libraries/Text/Text.d.ts @@ -229,6 +229,11 @@ export interface TextProps */ gradientAngle?: number | undefined; + /** + * Width of the gradient pattern in pixels. Default is 100. + */ + gradientWidth?: number | undefined; + /** * Width of the text stroke (outline). Creates an outer stroke effect. */ diff --git a/packages/react-native/Libraries/Text/TextNativeComponent.js b/packages/react-native/Libraries/Text/TextNativeComponent.js index 57f12efd7d7354..7d8a1bea601df4 100644 --- a/packages/react-native/Libraries/Text/TextNativeComponent.js +++ b/packages/react-native/Libraries/Text/TextNativeComponent.js @@ -49,6 +49,7 @@ const textViewConfig = { lineBreakStrategyIOS: true, gradientColors: true, gradientAngle: true, + gradientWidth: true, textStrokeWidth: true, textStrokeColor: true, }, @@ -67,6 +68,7 @@ const virtualTextViewConfig = { maxFontSizeMultiplier: true, gradientColors: true, gradientAngle: true, + gradientWidth: true, textStrokeWidth: true, textStrokeColor: true, }, 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 f97dbed7a34ffd..d07574c2cc1a79 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 @@ -177,7 +177,8 @@ private static void buildSpannedFromShadowNode( if (textShadowNode.mGradientColors != null && textShadowNode.mGradientColors.length >= 2) { int effectiveFontSize = textAttributes.getEffectiveFontSize(); float gradientAngle = Float.isNaN(textShadowNode.mGradientAngle) ? 0f : textShadowNode.mGradientAngle; - ops.add(new SetSpanOperation(start, end, new LinearGradientSpan(start * effectiveFontSize, textShadowNode.mGradientColors, gradientAngle))); + float gradientWidth = textShadowNode.mGradientWidth; + ops.add(new SetSpanOperation(start, end, new LinearGradientSpan(start * effectiveFontSize, textShadowNode.mGradientColors, gradientAngle, gradientWidth))); } if (textShadowNode.mIsBackgroundColorSet) { ops.add( @@ -351,6 +352,7 @@ protected Spannable spannedFromShadowNode( protected @Nullable int[] mGradientColors = null; protected float mGradientAngle = Float.NaN; + protected float mGradientWidth = Float.NaN; protected @Nullable AccessibilityRole mAccessibilityRole = null; protected @Nullable Role mRole = null; @@ -547,6 +549,12 @@ public void setGradientAngle(float gradientAngle) { markUpdated(); } + @ReactProp(name = "gradientWidth", defaultFloat = Float.NaN) + public void setGradientWidth(float gradientWidth) { + mGradientWidth = gradientWidth; + markUpdated(); + } + @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/TextAttributeProps.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java index fee5ecaff10661..49f5085790b76f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java @@ -69,6 +69,7 @@ public class TextAttributeProps { public static final short TA_KEY_TEXT_STROKE_WIDTH = 31; public static final short TA_KEY_TEXT_STROKE_COLOR = 32; public static final short TA_KEY_GRADIENT_ANGLE = 33; + public static final short TA_KEY_GRADIENT_WIDTH = 34; public static final int UNSET = -1; @@ -159,6 +160,7 @@ public class TextAttributeProps { protected @Nullable int[] mGradientColors = null; protected float mGradientAngle = Float.NaN; + protected float mGradientWidth = Float.NaN; private TextAttributeProps() {} @@ -255,6 +257,9 @@ public static TextAttributeProps fromMapBuffer(MapBuffer props) { case TA_KEY_GRADIENT_ANGLE: result.setGradientAngle((float) entry.getDoubleValue()); break; + case TA_KEY_GRADIENT_WIDTH: + result.setGradientWidth((float) entry.getDoubleValue()); + break; case TA_KEY_MAX_FONT_SIZE_MULTIPLIER: result.setMaxFontSizeMultiplier((float) entry.getDoubleValue()); break; @@ -303,6 +308,7 @@ public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) { result.setRole(getStringProp(props, ViewProps.ROLE)); result.setGradientColors(getArrayProp(props, "gradientColors")); result.setGradientAngle(getFloatProp(props, "gradientAngle", Float.NaN)); + result.setGradientWidth(getFloatProp(props, "gradientWidth", Float.NaN)); result.setTextStrokeWidth(getFloatProp(props, "textStrokeWidth", Float.NaN)); if (props.hasKey("textStrokeColor")) { result.setTextStrokeColor(props.getInt("textStrokeColor", 0)); @@ -824,6 +830,14 @@ private void setGradientAngle(float gradientAngle) { mGradientAngle = gradientAngle; } + public float getGradientWidth() { + return mGradientWidth; + } + + private void setGradientWidth(float gradientWidth) { + mGradientWidth = gradientWidth; + } + public static int getTextBreakStrategy(@Nullable String textBreakStrategy) { int androidTextBreakStrategy = DEFAULT_BREAK_STRATEGY; if (textBreakStrategy != null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt index 735dd1f87488eb..b9b48ccf4f043e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt @@ -259,11 +259,12 @@ internal object TextLayoutManager { if (textAttributes.gradientColors != null && textAttributes.gradientColors!!.size >= 2) { val effectiveFontSize = textAttributes.effectiveFontSize val gradientAngle = if (textAttributes.gradientAngle.isNaN()) 0f else textAttributes.gradientAngle + val gradientWidth = textAttributes.gradientWidth ops.add( SetSpanOperation( start, end, - LinearGradientSpan(start * effectiveFontSize.toFloat(), textAttributes.gradientColors!!, gradientAngle))) + LinearGradientSpan(start * effectiveFontSize.toFloat(), textAttributes.gradientColors!!, gradientAngle, gradientWidth))) } if (textAttributes.mIsBackgroundColorSet) { ops.add( @@ -418,8 +419,9 @@ internal object TextLayoutManager { if (fragment.props.gradientColors != null && fragment.props.gradientColors!!.size >= 2) { val effectiveFontSize = fragment.props.effectiveFontSize val gradientAngle = if (fragment.props.gradientAngle.isNaN()) 0f else fragment.props.gradientAngle + val gradientWidth = fragment.props.gradientWidth spannable.setSpan( - LinearGradientSpan(start * effectiveFontSize.toFloat(), fragment.props.gradientColors!!, gradientAngle), + LinearGradientSpan(start * effectiveFontSize.toFloat(), fragment.props.gradientColors!!, gradientAngle, gradientWidth), start, end, spanFlags) 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 index 2a50bb1557e27e..5628f4ffdee267 100644 --- 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 @@ -6,10 +6,19 @@ import android.text.TextPaint import android.text.style.CharacterStyle import android.text.style.UpdateAppearance +/** + * Span that applies a linear gradient to text. + * + * @param start The x-offset for the gradient start position + * @param colors Array of gradient colors + * @param angle Gradient angle in degrees (0 = horizontal) + * @param gradientWidth Width of the gradient pattern in pixels. Default is 100. + */ public class LinearGradientSpan( private val start: Float, private val colors: IntArray, private val angle: Float = 0f, + private val gradientWidth: Float = Float.NaN, ) : CharacterStyle(), ReactSpan, UpdateAppearance { public override fun updateDrawState(tp: TextPaint) { @@ -18,7 +27,7 @@ public class LinearGradientSpan( tp.setColor(colors[0]) val radians = Math.toRadians(angle.toDouble()) - val width = 100.0f + val width = if (gradientWidth.isNaN()) 100f else gradientWidth val height = tp.textSize val centerX = start + width / 2