Skip to content

Commit dca86ce

Browse files
evankatz14meta-codesync[bot]
authored andcommitted
Add font variation settings support to TextInput (#57805)
Summary: Pull Request resolved: #57805 Extend Android `TextInput` support for the existing `fontVariationSettings` text style prop. Fabric now forwards changes and resets through `AndroidTextInputProps`, and `ReactTextInputManager` applies the normalized value to the underlying `ReactEditText` paint after typeface updates. Using the `EditText` paint as the source of truth applies variable font axes consistently to placeholders, controlled values, and newly typed text. Equivalent `CustomStyleSpan`s are stripped while spans with different variation settings remain intact for attributed text ranges. As with `<Text>`, common text attributes forward settings syntax unchanged and Android remains the validation authority. This avoids narrowing Android-compatible grammar; malformed settings are outside the supported inheritance contract. Both `normal` and the React Native empty-string convention explicitly reset variation settings. Changelog: [Android][Added] - Add `fontVariationSettings` support for `TextInput` Reviewed By: Abbondanzo Differential Revision: D113580493
1 parent 978bdd0 commit dca86ce

6 files changed

Lines changed: 143 additions & 6 deletions

File tree

packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,7 @@ export type AndroidTextInputNativeProps = Readonly<{
615615
includeFontPadding?: ?boolean,
616616
fontWeight?: ?string,
617617
fontFamily?: ?string,
618+
fontVariationSettings?: ?string,
618619

619620
/**
620621
* I cannot find where these are defined but JS complains without them.
@@ -714,6 +715,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
714715
includeFontPadding: true,
715716
fontWeight: true,
716717
fontFamily: true,
718+
fontVariationSettings: true,
717719
allowFontScaling: true,
718720
onSelectionChange: true,
719721
mostRecentEventCount: true,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6358,6 +6358,7 @@ public class com/facebook/react/views/textinput/ReactTextInputManager : com/face
63586358
public final fun setFontSize (Lcom/facebook/react/views/textinput/ReactEditText;F)V
63596359
public final fun setFontStyle (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V
63606360
public final fun setFontVariant (Lcom/facebook/react/views/textinput/ReactEditText;Lcom/facebook/react/bridge/ReadableArray;)V
6361+
public final fun setFontVariationSettings (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V
63616362
public final fun setFontWeight (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V
63626363
public final fun setImportantForAutofill (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V
63636364
public final fun setIncludeFontPadding (Lcom/facebook/react/views/textinput/ReactEditText;Z)V

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

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import android.graphics.Canvas
1414
import android.graphics.Color
1515
import android.graphics.Paint
1616
import android.graphics.Rect
17+
import android.graphics.Typeface
1718
import android.os.Build
1819
import android.os.Bundle
1920
import android.text.Editable
@@ -47,6 +48,7 @@ import androidx.core.view.ViewCompat
4748
import com.facebook.common.logging.FLog
4849
import com.facebook.react.bridge.ReactSoftExceptionLogger.logSoftException
4950
import com.facebook.react.common.ReactConstants
51+
import com.facebook.react.common.assets.ReactFontManager
5052
import com.facebook.react.common.build.ReactBuildConfig
5153
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
5254
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
@@ -73,6 +75,7 @@ import com.facebook.react.views.text.ReactTextUpdate
7375
import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
7476
import com.facebook.react.views.text.ReactTypefaceUtils.getFontWeightAdjustment
7577
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
78+
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariationSettings
7679
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
7780
import com.facebook.react.views.text.TextAttributes
7881
import com.facebook.react.views.text.TextLayoutManager
@@ -135,6 +138,9 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
135138
private var fontFamily: String? = null
136139
private var fontWeight = ReactConstants.UNSET
137140
private var fontStyle = ReactConstants.UNSET
141+
internal var parsedFontVariationSettings: String? = null
142+
private set
143+
138144
private var autoFocus = false
139145
private var contextMenuHidden = false
140146
private var didAttachToWindow = false
@@ -646,6 +652,14 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
646652
}
647653
}
648654

655+
internal fun setReactFontVariationSettings(fontVariationSettings: String?) {
656+
val newParsedFontVariationSettings = parseFontVariationSettings(fontVariationSettings)
657+
if (newParsedFontVariationSettings != parsedFontVariationSettings) {
658+
parsedFontVariationSettings = newParsedFontVariationSettings
659+
typefaceDirty = true
660+
}
661+
}
662+
649663
override fun setFontFeatureSettings(fontFeatureSettings: String?) {
650664
if (fontFeatureSettings != getFontFeatureSettings()) {
651665
super.setFontFeatureSettings(fontFeatureSettings)
@@ -662,14 +676,18 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
662676

663677
val newTypeface = applyStyles(typeface, fontStyle, fontWeight, fontFamily, context.assets)
664678
typeface = newTypeface
679+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
680+
super.setFontVariationSettings(parsedFontVariationSettings)
681+
}
665682

666683
// Match behavior of CustomStyleSpan and enable SUBPIXEL_TEXT_FLAG when setting anything
667684
// nonstandard
668685
val enableSubpixelText =
669686
fontStyle != ReactConstants.UNSET ||
670687
fontWeight != ReactConstants.UNSET ||
671688
fontFamily != null ||
672-
fontFeatureSettings != null
689+
fontFeatureSettings != null ||
690+
parsedFontVariationSettings != null
673691
paintFlags =
674692
if (enableSubpixelText) {
675693
paintFlags or Paint.SUBPIXEL_TEXT_FLAG
@@ -822,12 +840,16 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
822840
span.spacing == textAttributes.effectiveLetterSpacing
823841
}
824842

843+
val effectiveFontStyle = if (fontStyle == ReactConstants.UNSET) Typeface.NORMAL else fontStyle
844+
val effectiveFontWeight =
845+
if (fontWeight == ReactConstants.UNSET) ReactFontManager.TypefaceStyle.NORMAL
846+
else fontWeight
825847
stripSpansOfKind(sb, CustomStyleSpan::class.java) { span: CustomStyleSpan ->
826-
span.style == fontStyle &&
848+
span.style == effectiveFontStyle &&
827849
span.fontFamily == fontFamily &&
828-
span.weight == fontWeight &&
850+
span.weight == effectiveFontWeight &&
829851
span.fontFeatureSettings == fontFeatureSettings &&
830-
span.fontVariationSettings == null
852+
span.fontVariationSettings == parsedFontVariationSettings
831853
}
832854
}
833855

@@ -902,14 +924,15 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
902924
fontStyle != ReactConstants.UNSET ||
903925
fontWeight != ReactConstants.UNSET ||
904926
fontFamily != null ||
905-
fontFeatureSettings != null
927+
fontFeatureSettings != null ||
928+
parsedFontVariationSettings != null
906929
) {
907930
workingText.setSpan(
908931
CustomStyleSpan(
909932
fontStyle,
910933
fontWeight,
911934
fontFeatureSettings,
912-
null,
935+
parsedFontVariationSettings,
913936
fontFamily,
914937
context.assets,
915938
getFontWeightAdjustment(context),

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,11 @@ public open class ReactTextInputManager public constructor() :
256256
view.fontFeatureSettings = parseFontVariant(fontVariant)
257257
}
258258

259+
@ReactProp(name = ViewProps.FONT_VARIATION_SETTINGS)
260+
public fun setFontVariationSettings(view: ReactEditText, fontVariationSettings: String?) {
261+
view.setReactFontVariationSettings(fontVariationSettings)
262+
}
263+
259264
@ReactProp(name = ViewProps.INCLUDE_FONT_PADDING, defaultBoolean = true)
260265
public fun setIncludeFontPadding(view: ReactEditText, includepad: Boolean) {
261266
view.includeFontPadding = includepad

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import android.util.DisplayMetrics
2323
import android.view.Gravity
2424
import android.view.View
2525
import android.view.inputmethod.EditorInfo
26+
import androidx.annotation.RequiresApi
2627
import androidx.autofill.HintConstants
2728
import androidx.core.content.res.ResourcesCompat.ID_NULL
2829
import com.facebook.react.bridge.BridgeReactContext
@@ -35,6 +36,7 @@ import com.facebook.react.uimanager.ReactStylesDiffMap
3536
import com.facebook.react.uimanager.ThemedReactContext
3637
import com.facebook.react.views.text.DefaultStyleValuesUtil.getDefaultTextColorHint
3738
import com.facebook.react.views.text.ReactTextUpdate
39+
import com.facebook.react.views.text.internal.span.CustomStyleSpan
3840
import org.assertj.core.api.Assertions.assertThat
3941
import org.junit.Before
4042
import org.junit.Test
@@ -497,6 +499,98 @@ class ReactTextInputPropertyTest {
497499
assertThat(view.filters).isEqualTo(filters)
498500
}
499501

502+
@Test
503+
fun testFontVariationSettings() {
504+
manager.updateProperties(view, buildStyles("fontVariationSettings", "'wght' 550"))
505+
assertThat(view.parsedFontVariationSettings).isEqualTo("'wght' 550")
506+
507+
manager.updateProperties(view, buildStyles("fontVariationSettings", "invalid"))
508+
assertThat(view.parsedFontVariationSettings).isNull()
509+
510+
manager.updateProperties(view, buildStyles("fontVariationSettings", "normal"))
511+
assertThat(view.parsedFontVariationSettings).isEmpty()
512+
513+
manager.updateProperties(view, buildStyles("fontVariationSettings", ""))
514+
assertThat(view.parsedFontVariationSettings).isEmpty()
515+
516+
manager.updateProperties(view, buildStyles("fontVariationSettings", null))
517+
assertThat(view.parsedFontVariationSettings).isNull()
518+
}
519+
520+
@Test
521+
fun testFontVariationSettingsOverrideFontWeightRegardlessOfPropOrder() {
522+
manager.updateProperties(
523+
view,
524+
buildStyles(
525+
"fontVariationSettings",
526+
"'wght' 450",
527+
"fontWeight",
528+
"700",
529+
),
530+
)
531+
assertThat(view.parsedFontVariationSettings).isEqualTo("'wght' 450")
532+
533+
manager.updateProperties(
534+
view,
535+
buildStyles(
536+
"fontWeight",
537+
"300",
538+
"fontVariationSettings",
539+
"'wght' 550",
540+
),
541+
)
542+
assertThat(view.parsedFontVariationSettings).isEqualTo("'wght' 550")
543+
}
544+
545+
@RequiresApi(Build.VERSION_CODES.M)
546+
@Test
547+
fun testFontVariationSettingsStripOnlyEquivalentSpans() {
548+
manager.updateProperties(
549+
view,
550+
buildStyles(
551+
"fontFamily",
552+
"sans-serif",
553+
"fontVariationSettings",
554+
"'wght' 550",
555+
),
556+
)
557+
val matchingSpan = CustomStyleSpan(
558+
0,
559+
400,
560+
view.fontFeatureSettings,
561+
"'wght' 550",
562+
"sans-serif",
563+
themedContext.assets,
564+
)
565+
val differingSpan = CustomStyleSpan(
566+
0,
567+
400,
568+
view.fontFeatureSettings,
569+
"'wght' 700",
570+
"sans-serif",
571+
themedContext.assets,
572+
)
573+
val textUpdate =
574+
SpannableString("matching different").apply {
575+
setSpan(matchingSpan, 0, 8, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
576+
setSpan(differingSpan, 9, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
577+
}
578+
579+
view.maybeSetTextFromState(
580+
ReactTextUpdate(
581+
textUpdate,
582+
0,
583+
view.gravity and Gravity.HORIZONTAL_GRAVITY_MASK,
584+
Layout.BREAK_STRATEGY_HIGH_QUALITY,
585+
0,
586+
)
587+
)
588+
589+
val remainingSpans =
590+
checkNotNull(view.text).getSpans(0, view.length(), CustomStyleSpan::class.java)
591+
assertThat(remainingSpans).containsExactly(differingSpan)
592+
}
593+
500594
@Test
501595
fun testSecureTextDoesNotReplaceSameTextFromJS() {
502596
val markerSpan = MarkerSpan()

packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,10 @@ folly::dynamic AndroidTextInputProps::getDynamic() const {
323323
props["includeFontPadding"] = includeFontPadding;
324324
props["fontWeight"] = fontWeight;
325325
props["fontFamily"] = fontFamily;
326+
props["fontVariationSettings"] =
327+
textAttributes.fontVariationSettings.has_value()
328+
? folly::dynamic(*textAttributes.fontVariationSettings)
329+
: folly::dynamic(nullptr);
326330
props["cursorColor"] = toAndroidRepr(cursorColor);
327331
props["mostRecentEventCount"] = mostRecentEventCount;
328332
props["text"] = text;
@@ -622,6 +626,14 @@ folly::dynamic AndroidTextInputProps::getDiffProps(
622626
result["fontFamily"] = fontFamily;
623627
}
624628

629+
if (textAttributes.fontVariationSettings !=
630+
oldProps->textAttributes.fontVariationSettings) {
631+
result["fontVariationSettings"] =
632+
textAttributes.fontVariationSettings.has_value()
633+
? folly::dynamic(*textAttributes.fontVariationSettings)
634+
: folly::dynamic(nullptr);
635+
}
636+
625637
return result;
626638
}
627639

0 commit comments

Comments
 (0)