Skip to content

Commit bf3cb88

Browse files
evankatz14facebook-github-bot
authored andcommitted
Add Android text font variation settings (#57804)
Summary: Add a `fontVariationSettings` text style prop and carry it through Fabric text attributes into Android text rendering. Android now deserializes the prop for `<Text>`, applies it to `Paint`, and includes it in text measurement cache identity because variable axes can affect layout. Preserve the distinction between an absent setting and an explicitly empty setting so nested text can inherit or clear the parent variation axes. Apply high-level font properties before low-level variation settings so explicit axes take precedence, matching CSS font realization order. Settings syntax is intentionally forwarded unchanged through common text attributes and validated only by the Android font variation parser. This avoids narrowing the grammar Android accepts. As a result, malformed child settings are outside the supported inheritance contract: they replace an inherited value before Android validation and are not guaranteed to fall back to the parent settings. Android also accepts `normal` and the React Native empty-string convention as explicit resets. Changelog: [Android][Added] - Add `fontVariationSettings` support for `<Text>` Reviewed By: Abbondanzo Differential Revision: D113580491
1 parent 917d97e commit bf3cb88

29 files changed

Lines changed: 369 additions & 9 deletions

packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
258258
fontSize: true,
259259
fontStyle: true,
260260
fontVariant: fontVariantAttribute,
261+
fontVariationSettings: true,
261262
fontWeight: true,
262263
includeFontPadding: true,
263264
letterSpacing: true,

packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,12 @@ export interface TextStyle extends TextStyleIOS, TextStyleAndroid, ViewStyle {
601601
| 'heavy'
602602
| 'black'
603603
| undefined;
604+
/**
605+
* Specifies OpenType font variation axis values using CSS syntax. An empty
606+
* string resets inherited variation settings. On Android, this requires API
607+
* level 26 or later.
608+
*/
609+
fontVariationSettings?: string | undefined;
604610
letterSpacing?: number | undefined;
605611
lineHeight?: number | undefined;
606612
textAlign?:

packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,12 @@ type ____TextStyle_InternalBase = Readonly<{
10111011
*/
10121012
fontWeight?: ____FontWeight_Internal,
10131013
fontVariant?: ____FontVariantArray_Internal | string,
1014+
/**
1015+
* Specifies OpenType font variation axis values using CSS syntax. An empty
1016+
* string resets inherited variation settings. On Android, this requires API
1017+
* level 26 or later.
1018+
*/
1019+
fontVariationSettings?: string,
10141020
textShadowOffset?: Readonly<{
10151021
width: number,
10161022
height: number,

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4543,6 +4543,7 @@ public final class com/facebook/react/uimanager/ViewProps {
45434543
public static final field FONT_SIZE Ljava/lang/String;
45444544
public static final field FONT_STYLE Ljava/lang/String;
45454545
public static final field FONT_VARIANT Ljava/lang/String;
4546+
public static final field FONT_VARIATION_SETTINGS Ljava/lang/String;
45464547
public static final field FONT_WEIGHT Ljava/lang/String;
45474548
public static final field FOREGROUND_COLOR Ljava/lang/String;
45484549
public static final field GAP Ljava/lang/String;
@@ -6137,6 +6138,7 @@ public final class com/facebook/react/views/text/TextAttributeProps {
61376138
public static final field TA_KEY_FONT_SIZE_MULTIPLIER I
61386139
public static final field TA_KEY_FONT_STYLE I
61396140
public static final field TA_KEY_FONT_VARIANT I
6141+
public static final field TA_KEY_FONT_VARIATION_SETTINGS I
61406142
public static final field TA_KEY_FONT_WEIGHT I
61416143
public static final field TA_KEY_FOREGROUND_COLOR I
61426144
public static final field TA_KEY_IS_HIGHLIGHTED I
@@ -6167,6 +6169,7 @@ public final class com/facebook/react/views/text/TextAttributeProps {
61676169
public final fun getFontFeatureSettings ()Ljava/lang/String;
61686170
public final fun getFontSize ()I
61696171
public final fun getFontStyle ()I
6172+
public final fun getFontVariationSettings ()Ljava/lang/String;
61706173
public final fun getFontWeight ()I
61716174
public final fun getLayoutDirection ()I
61726175
public final fun getLetterSpacing ()F

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public object ViewProps {
8484
public const val FONT_WEIGHT: String = "fontWeight"
8585
public const val FONT_STYLE: String = "fontStyle"
8686
public const val FONT_VARIANT: String = "fontVariant"
87+
public const val FONT_VARIATION_SETTINGS: String = "fontVariationSettings"
8788
public const val FONT_FAMILY: String = "fontFamily"
8889
public const val LINE_HEIGHT: String = "lineHeight"
8990
public const val LETTER_SPACING: String = "letterSpacing"

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ package com.facebook.react.views.text
1010
import android.content.Context
1111
import android.content.res.AssetManager
1212
import android.content.res.Configuration
13+
import android.graphics.Paint
1314
import android.graphics.Typeface
15+
import android.graphics.fonts.FontVariationAxis
1416
import android.os.Build
17+
import com.facebook.common.logging.FLog
1518
import com.facebook.react.bridge.ReadableArray
1619
import com.facebook.react.common.ReactConstants
1720
import com.facebook.react.common.assets.ReactFontManager
@@ -151,4 +154,44 @@ public object ReactTypefaceUtils {
151154

152155
return Typeface.create(baseTypeface, adjustedWeight, italic)
153156
}
157+
158+
internal fun parseFontVariationSettings(fontVariationSettings: String?): String? {
159+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
160+
return null
161+
}
162+
163+
if (fontVariationSettings.isNullOrEmpty()) {
164+
return fontVariationSettings
165+
}
166+
167+
if (fontVariationSettings.trim().equals("normal", ignoreCase = true)) {
168+
return ""
169+
}
170+
171+
return try {
172+
FontVariationAxis.fromFontVariationSettings(fontVariationSettings)
173+
fontVariationSettings
174+
} catch (exception: IllegalArgumentException) {
175+
FLog.w(ReactConstants.TAG, "Invalid fontVariationSettings: $fontVariationSettings")
176+
null
177+
}
178+
}
179+
180+
internal fun applyFontVariationSettings(paint: Paint, fontVariationSettings: String?) {
181+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
182+
return
183+
}
184+
185+
try {
186+
paint.setFontVariationSettings(fontVariationSettings)
187+
} catch (exception: IllegalArgumentException) {
188+
// Paint instances are reused, so explicitly clear axes from a previous layout.
189+
paint.setFontVariationSettings(null)
190+
FLog.w(
191+
ReactConstants.TAG,
192+
"Invalid fontVariationSettings: $fontVariationSettings",
193+
exception,
194+
)
195+
}
196+
}
154197
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.facebook.react.uimanager.ReactStylesDiffMap
2525
import com.facebook.react.uimanager.ViewProps
2626
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
2727
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariant
28+
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariationSettings
2829
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
2930
import kotlin.math.ceil
3031

@@ -153,6 +154,10 @@ public class TextAttributeProps private constructor() {
153154
public var fontFeatureSettings: String? = null
154155
private set
155156

157+
/** @see android.graphics.Paint.setFontVariationSettings */
158+
public var fontVariationSettings: String? = null
159+
private set
160+
156161
@Deprecated("Use lineHeight instead", ReplaceWith("lineHeight"))
157162
public val effectiveLineHeight: Float
158163
get() = lineHeight
@@ -396,6 +401,7 @@ public class TextAttributeProps private constructor() {
396401
public const val TA_KEY_TEXT_TRANSFORM: Int = 27
397402
public const val TA_KEY_MAX_FONT_SIZE_MULTIPLIER: Int = 29
398403
public const val TA_KEY_TEXT_EFFECTS: Int = 30
404+
public const val TA_KEY_FONT_VARIATION_SETTINGS: Int = 31
399405
private const val TE_KEY_NAME: Int = 0
400406
private const val TE_KEY_PROPS: Int = 1
401407

@@ -433,6 +439,8 @@ public class TextAttributeProps private constructor() {
433439
TA_KEY_FONT_WEIGHT -> result.setFontWeight(entry.stringValue)
434440
TA_KEY_FONT_STYLE -> result.setFontStyle(entry.stringValue)
435441
TA_KEY_FONT_VARIANT -> result.setFontVariant(entry.mapBufferValue)
442+
TA_KEY_FONT_VARIATION_SETTINGS ->
443+
result.fontVariationSettings = parseFontVariationSettings(entry.stringValue)
436444
TA_KEY_ALLOW_FONT_SCALING -> result.allowFontScaling = entry.booleanValue
437445
TA_KEY_LETTER_SPACING -> result.letterSpacing = entry.doubleValue.toFloat()
438446
TA_KEY_LINE_HEIGHT -> result.lineHeight = entry.doubleValue.toFloat()
@@ -500,6 +508,8 @@ public class TextAttributeProps private constructor() {
500508
result.setFontWeight(getStringProp(props, ViewProps.FONT_WEIGHT))
501509
result.setFontStyle(getStringProp(props, ViewProps.FONT_STYLE))
502510
result.setFontVariant(getArrayProp(props, ViewProps.FONT_VARIANT))
511+
result.fontVariationSettings =
512+
parseFontVariationSettings(getStringProp(props, ViewProps.FONT_VARIATION_SETTINGS))
503513
result.includeFontPadding = getBooleanProp(props, ViewProps.INCLUDE_FONT_PADDING, true)
504514
result.setTextDecorationLine(getStringProp(props, ViewProps.TEXT_DECORATION_LINE))
505515
result.textDecorationColor =

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ internal object TextLayoutManager {
313313
if (
314314
textAttributes.fontStyle != ReactConstants.UNSET ||
315315
textAttributes.fontWeight != ReactConstants.UNSET ||
316+
textAttributes.fontVariationSettings != null ||
316317
textAttributes.fontFamily != null
317318
) {
318319
ops.add(
@@ -323,6 +324,7 @@ internal object TextLayoutManager {
323324
textAttributes.fontStyle,
324325
textAttributes.fontWeight,
325326
textAttributes.fontFeatureSettings,
327+
textAttributes.fontVariationSettings,
326328
textAttributes.fontFamily,
327329
assets,
328330
fontWeightAdjustment,
@@ -547,13 +549,15 @@ internal object TextLayoutManager {
547549
if (
548550
fragment.props.fontStyle != ReactConstants.UNSET ||
549551
fragment.props.fontWeight != ReactConstants.UNSET ||
552+
fragment.props.fontVariationSettings != null ||
550553
fragment.props.fontFamily != null
551554
) {
552555
spannable.setSpan(
553556
CustomStyleSpan(
554557
fragment.props.fontStyle,
555558
fragment.props.fontWeight,
556559
fragment.props.fontFeatureSettings,
560+
fragment.props.fontVariationSettings,
557561
fragment.props.fontFamily,
558562
assets,
559563
fontWeightAdjustment,
@@ -883,6 +887,7 @@ internal object TextLayoutManager {
883887
if (
884888
baseTextAttributes.fontStyle != ReactConstants.UNSET ||
885889
baseTextAttributes.fontWeight != ReactConstants.UNSET ||
890+
baseTextAttributes.fontVariationSettings != null ||
886891
baseTextAttributes.fontFamily != null
887892
) {
888893
val typeface =
@@ -912,6 +917,8 @@ internal object TextLayoutManager {
912917
paint.setTypeface(typeface)
913918
}
914919
}
920+
921+
ReactTypefaceUtils.applyFontVariationSettings(paint, baseTextAttributes.fontVariationSettings)
915922
}
916923

917924
/**

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CustomStyleSpan.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal class CustomStyleSpan(
3131
private val privateStyle: Int,
3232
private val privateWeight: Int,
3333
val fontFeatureSettings: String?,
34+
val fontVariationSettings: String?,
3435
val fontFamily: String?,
3536
private val assetManager: AssetManager,
3637
private val fontWeightAdjustment: Int = 0,
@@ -41,6 +42,7 @@ internal class CustomStyleSpan(
4142
privateStyle,
4243
privateWeight,
4344
fontFeatureSettings,
45+
fontVariationSettings,
4446
fontFamily,
4547
assetManager,
4648
fontWeightAdjustment,
@@ -53,6 +55,7 @@ internal class CustomStyleSpan(
5355
privateStyle,
5456
privateWeight,
5557
fontFeatureSettings,
58+
fontVariationSettings,
5659
fontFamily,
5760
assetManager,
5861
fontWeightAdjustment,
@@ -81,6 +84,7 @@ internal class CustomStyleSpan(
8184
style: Int,
8285
weight: Int,
8386
fontFeatureSettingsParam: String?,
87+
fontVariationSettingsParam: String?,
8488
family: String?,
8589
assetManager: AssetManager,
8690
fontWeightAdjustment: Int,
@@ -92,6 +96,7 @@ internal class CustomStyleSpan(
9296
paint.apply {
9397
fontFeatureSettings = fontFeatureSettingsParam
9498
setTypeface(adjustedTypeface)
99+
ReactTypefaceUtils.applyFontVariationSettings(this, fontVariationSettingsParam)
95100
isSubpixelText = true
96101
isLinearText = true
97102
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,8 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
826826
span.style == fontStyle &&
827827
span.fontFamily == fontFamily &&
828828
span.weight == fontWeight &&
829-
span.fontFeatureSettings == fontFeatureSettings
829+
span.fontFeatureSettings == fontFeatureSettings &&
830+
span.fontVariationSettings == null
830831
}
831832
}
832833

@@ -908,6 +909,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
908909
fontStyle,
909910
fontWeight,
910911
fontFeatureSettings,
912+
null,
911913
fontFamily,
912914
context.assets,
913915
getFontWeightAdjustment(context),

0 commit comments

Comments
 (0)