diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 3fe95a4595db..51a486adc1a5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -559,6 +559,7 @@ private NativeArray measureLines( TextLayoutManager.measureLines( mReactApplicationContext.getAssets(), ReactTypefaceUtils.getFontWeightAdjustment(mReactApplicationContext), + mReactApplicationContext, attributedString, paragraphAttributes, PixelUtil.toPixelFromDIP(width), @@ -648,6 +649,7 @@ public long measureText( return TextLayoutManager.measureText( mReactApplicationContext.getAssets(), ReactTypefaceUtils.getFontWeightAdjustment(mReactApplicationContext), + mReactApplicationContext, attributedString, paragraphAttributes, getYogaSize(minWidth, maxWidth), @@ -676,6 +678,7 @@ public PreparedLayout prepareTextLayout( return TextLayoutManager.createPreparedLayout( mReactApplicationContext.getAssets(), ReactTypefaceUtils.getFontWeightAdjustment(mReactApplicationContext), + mReactApplicationContext, attributedString, paragraphAttributes, getYogaSize(minWidth, maxWidth), 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 53a67377158f..ddfd1a97fb9c 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 @@ -7,7 +7,9 @@ package com.facebook.react.views.text +import android.content.Context import android.content.res.AssetManager +import android.content.res.Configuration import android.graphics.Color import android.graphics.RectF import android.graphics.Typeface @@ -25,6 +27,7 @@ import android.text.TextUtils import android.util.LayoutDirection import android.view.Gravity import android.view.View +import android.widget.TextView import androidx.annotation.VisibleForTesting import com.facebook.common.logging.FLog import com.facebook.infer.annotation.Assertions @@ -1008,6 +1011,7 @@ internal object TextLayoutManager { baseTextAttributes: TextAttributeProps, assets: AssetManager, fontWeightAdjustment: Int, + context: Context, ) { if (baseTextAttributes.fontSize != ReactConstants.UNSET) { paint.textSize = baseTextAttributes.fontSize.toFloat() @@ -1044,12 +1048,40 @@ internal object TextLayoutManager { val typeface = ReactTypefaceUtils.applyFontWeightAdjustment(null, fontWeightAdjustment) if (typeface != null) { paint.setTypeface(typeface) + } else { + // ReactTextView renders text with a plain TextView, whose default typeface + // inherits the system/theme font configuration (e.g. a bold system font or a + // font-replacement app). Measurement uses a bare TextPaint whose default + // typeface is Typeface.DEFAULT; when these diverge, auto-width is + // measured narrower than it renders, clipping the last glyphs. Use the same + // default typeface the rendering TextView uses. + paint.setTypeface(getSystemDefaultTypeface(context)) } } ReactTypefaceUtils.applyFontVariationSettings(paint, baseTextAttributes.fontVariationSettings) } + private var sSystemDefaultTypeface: Typeface? = null + private var sSystemDefaultTypefaceConfiguration: Configuration? = null + + @Synchronized + private fun getSystemDefaultTypeface(context: Context): Typeface { + val config = context.resources.configuration + if ( + sSystemDefaultTypeface == null || + sSystemDefaultTypefaceConfiguration?.equals(config) != true + ) { + // Create a single TextView (no per-measure allocation) from the application + // context so we read the same default typeface the rendering TextView uses, + // including system/theme-level font replacements. Re-resolve whenever the + // Configuration (fontScale, fontWeightAdjustment, locale, ...) changes. + sSystemDefaultTypeface = TextView(context.applicationContext).typeface + sSystemDefaultTypefaceConfiguration = Configuration(config) + } + return checkNotNull(sSystemDefaultTypeface) + } + /** * WARNING: This paint should not be used for any layouts which may escape TextLayoutManager, as * they may need to be drawn later, and may not safely be reused @@ -1058,13 +1090,14 @@ internal object TextLayoutManager { baseTextAttributes: TextAttributeProps, assets: AssetManager, fontWeightAdjustment: Int, + context: Context, ): TextPaint { val paint = checkNotNull(textPaintInstance.get()) paint.setTypeface(null) paint.textSize = 12f paint.isFakeBoldText = false paint.textSkewX = 0f - updateTextPaint(paint, baseTextAttributes, assets, fontWeightAdjustment) + updateTextPaint(paint, baseTextAttributes, assets, fontWeightAdjustment, context) return paint } @@ -1072,9 +1105,10 @@ internal object TextLayoutManager { baseTextAttributes: TextAttributeProps, assets: AssetManager, fontWeightAdjustment: Int, + context: Context, ): TextPaint { val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG) - updateTextPaint(paint, baseTextAttributes, assets, fontWeightAdjustment) + updateTextPaint(paint, baseTextAttributes, assets, fontWeightAdjustment, context) return paint } @@ -1082,6 +1116,7 @@ internal object TextLayoutManager { private fun createLayoutForMeasurement( assets: AssetManager, fontWeightAdjustment: Int, + context: Context, attributedString: MapBuffer, paragraphAttributes: MapBuffer, width: Float, @@ -1106,7 +1141,8 @@ internal object TextLayoutManager { } else { val baseTextAttributes = TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES)) - paint = scratchPaintWithAttributes(baseTextAttributes, assets, fontWeightAdjustment) + paint = + scratchPaintWithAttributes(baseTextAttributes, assets, fontWeightAdjustment, context) } return createLayout( @@ -1245,6 +1281,7 @@ internal object TextLayoutManager { @OptIn(UnstableReactNativeAPI::class) fun createPreparedLayout( assets: AssetManager, + context: Context, attributedString: ReadableMapBuffer, paragraphAttributes: ReadableMapBuffer, width: Float, @@ -1257,6 +1294,7 @@ internal object TextLayoutManager { createPreparedLayout( assets, 0, + context, attributedString, paragraphAttributes, width, @@ -1272,6 +1310,7 @@ internal object TextLayoutManager { fun createPreparedLayout( assets: AssetManager, fontWeightAdjustment: Int, + context: Context, attributedString: ReadableMapBuffer, paragraphAttributes: ReadableMapBuffer, width: Float, @@ -1297,7 +1336,7 @@ internal object TextLayoutManager { val result = createLayout( text, - newPaintWithAttributes(baseTextAttributes, assets, fontWeightAdjustment), + newPaintWithAttributes(baseTextAttributes, assets, fontWeightAdjustment, context), attributedString, paragraphAttributes, width, @@ -1439,6 +1478,7 @@ internal object TextLayoutManager { @OptIn(UnstableReactNativeAPI::class) fun measureText( assets: AssetManager, + context: Context, attributedString: MapBuffer, paragraphAttributes: MapBuffer, width: Float, @@ -1452,6 +1492,7 @@ internal object TextLayoutManager { measureText( assets, 0, + context, attributedString, paragraphAttributes, width, @@ -1468,6 +1509,7 @@ internal object TextLayoutManager { fun measureText( assets: AssetManager, fontWeightAdjustment: Int, + context: Context, attributedString: MapBuffer, paragraphAttributes: MapBuffer, width: Float, @@ -1483,6 +1525,7 @@ internal object TextLayoutManager { createLayoutForMeasurement( assets, fontWeightAdjustment, + context, attributedString, paragraphAttributes, width, @@ -1754,6 +1797,7 @@ internal object TextLayoutManager { @OptIn(UnstableReactNativeAPI::class) fun measureLines( assetManager: AssetManager, + context: Context, attributedString: MapBuffer, paragraphAttributes: MapBuffer, width: Float, @@ -1764,6 +1808,7 @@ internal object TextLayoutManager { measureLines( assetManager, 0, + context, attributedString, paragraphAttributes, width, @@ -1777,6 +1822,7 @@ internal object TextLayoutManager { fun measureLines( assetManager: AssetManager, fontWeightAdjustment: Int, + context: Context, attributedString: MapBuffer, paragraphAttributes: MapBuffer, width: Float, @@ -1788,6 +1834,7 @@ internal object TextLayoutManager { createLayoutForMeasurement( assetManager, fontWeightAdjustment, + context, attributedString, paragraphAttributes, width, diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextLayoutManagerFontWeightAdjustmentTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextLayoutManagerFontWeightAdjustmentTest.kt index 7fadd9b556da..751d7d69f573 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextLayoutManagerFontWeightAdjustmentTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextLayoutManagerFontWeightAdjustmentTest.kt @@ -11,6 +11,7 @@ import android.graphics.Paint import android.graphics.Typeface import android.os.Build import android.text.TextPaint +import android.widget.TextView import androidx.annotation.RequiresApi import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.uimanager.DisplayMetricsHolder @@ -56,13 +57,14 @@ class TextLayoutManagerFontWeightAdjustmentTest { textAttributes, RuntimeEnvironment.getApplication().assets, FONT_WEIGHT_ADJUSTMENT_BOLD_TEXT, + RuntimeEnvironment.getApplication(), ) assertThat(paint.typeface).isNotNull } @Test - fun `plain text paint keeps default typeface unset without font weight adjustment`() { + fun `plain text paint uses rendering TextView default typeface without font weight adjustment`() { val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG) val textAttributes = TextAttributeProps.fromReadableMap(ReactStylesDiffMap(JavaOnlyMap())) @@ -71,9 +73,15 @@ class TextLayoutManagerFontWeightAdjustmentTest { textAttributes, RuntimeEnvironment.getApplication().assets, 0, + RuntimeEnvironment.getApplication(), ) - assertThat(paint.typeface).isNull() + // Matches the typeface the rendering TextView (ReactTextView) would use, so + // measurement and rendering stay in sync even when the system default font is + // replaced (e.g. a bold system font). + assertThat(paint.typeface) + .isNotNull + .isEqualTo(TextView(RuntimeEnvironment.getApplication()).typeface) } @Test @@ -89,6 +97,7 @@ class TextLayoutManagerFontWeightAdjustmentTest { textAttributes, RuntimeEnvironment.getApplication().assets, 0, + RuntimeEnvironment.getApplication(), ) assertThat(paint.fontVariationSettings).isNull()