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 ee64131ab187..6bc5d8f60f65 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 @@ -93,6 +93,7 @@ import com.facebook.react.views.text.PreparedLayout; import com.facebook.react.views.text.ReactTextViewManager; import com.facebook.react.views.text.ReactTextViewManagerCallback; +import com.facebook.react.views.text.ReactTypefaceUtils; import com.facebook.react.views.text.TextEffectRegistry; import com.facebook.react.views.text.TextLayoutManager; import java.util.ArrayList; @@ -553,6 +554,7 @@ private NativeArray measureLines( return (NativeArray) TextLayoutManager.measureLines( mReactApplicationContext.getAssets(), + ReactTypefaceUtils.getFontWeightAdjustment(mReactApplicationContext), attributedString, paragraphAttributes, PixelUtil.toPixelFromDIP(width), @@ -639,6 +641,7 @@ public long measureText( return TextLayoutManager.measureText( mReactApplicationContext.getAssets(), + ReactTypefaceUtils.getFontWeightAdjustment(mReactApplicationContext), attributedString, paragraphAttributes, getYogaSize(minWidth, maxWidth), @@ -666,6 +669,7 @@ public PreparedLayout prepareTextLayout( return TextLayoutManager.createPreparedLayout( mReactApplicationContext.getAssets(), + ReactTypefaceUtils.getFontWeightAdjustment(mReactApplicationContext), attributedString, paragraphAttributes, getYogaSize(minWidth, maxWidth), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt index bc420d79c67f..2d06fcdab421 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt @@ -158,6 +158,7 @@ public constructor( val spanned: Spannable = TextLayoutManager.getOrCreateSpannableForText( view.context.assets, + ReactTypefaceUtils.getFontWeightAdjustment(view.context), attributedString, reactTextViewManagerCallback, TextEffectRegistry.current, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.kt index e33daa691f31..f66c98117ff4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.kt @@ -7,11 +7,17 @@ package com.facebook.react.views.text +import android.content.Context import android.content.res.AssetManager +import android.content.res.Configuration import android.graphics.Typeface +import android.graphics.fonts.FontStyle +import android.os.Build import com.facebook.react.bridge.ReadableArray import com.facebook.react.common.ReactConstants import com.facebook.react.common.assets.ReactFontManager +import kotlin.math.max +import kotlin.math.min public object ReactTypefaceUtils { @@ -110,4 +116,36 @@ public object ReactTypefaceUtils { ReactFontManager.getInstance().getTypeface(fontFamilyName, typefaceStyle, assetManager) } } + + @JvmStatic + public fun getFontWeightAdjustment(context: Context): Int = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + context.resources.configuration.fontWeightAdjustment + } else { + 0 + } + + @JvmStatic + public fun applyFontWeightAdjustment( + typeface: Typeface?, + fontWeightAdjustment: Int, + ): Typeface? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || fontWeightAdjustment == 0) { + return typeface + } + + if (fontWeightAdjustment == Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) { + return typeface + } + + val baseTypeface = typeface ?: Typeface.DEFAULT + val adjustedWeight = + min( + max(baseTypeface.weight + fontWeightAdjustment, FontStyle.FONT_WEIGHT_MIN), + FontStyle.FONT_WEIGHT_MAX, + ) + val italic = baseTypeface.style and Typeface.ITALIC != 0 + + return Typeface.create(baseTypeface, adjustedWeight, italic) + } } 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 4a900ffcf711..015dff13801d 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 @@ -109,7 +109,12 @@ internal object TextLayoutManager { private const val DEFAULT_ADJUST_FONT_SIZE_TO_FIT = false - private val tagToSpannableCache = ConcurrentHashMap() + private data class CachedSpannable( + val spannable: Spannable, + val fontWeightAdjustment: Int, + ) + + private val tagToSpannableCache = ConcurrentHashMap() // Lazily cached Method for StaticLayout.Builder.setUseBoundsForWidth (API 35+). // Reflection is needed because some internal targets compile against an SDK older than 35. @@ -123,8 +128,12 @@ internal object TextLayoutManager { } } - fun setCachedSpannableForTag(reactTag: Int, sp: Spannable): Unit { - tagToSpannableCache[reactTag] = sp + fun setCachedSpannableForTag( + reactTag: Int, + fontWeightAdjustment: Int, + sp: Spannable, + ): Unit { + tagToSpannableCache[reactTag] = CachedSpannable(sp, fontWeightAdjustment) } fun deleteCachedSpannableForTag(reactTag: Int): Unit { @@ -238,6 +247,7 @@ internal object TextLayoutManager { @OptIn(UnstableReactNativeAPI::class) private fun buildSpannableFromFragments( assets: AssetManager, + fontWeightAdjustment: Int, fragments: MapBuffer, sb: SpannableStringBuilder, ops: MutableList, @@ -323,6 +333,7 @@ internal object TextLayoutManager { textAttributes.fontFeatureSettings, textAttributes.fontFamily, assets, + fontWeightAdjustment, ), ) ) @@ -426,6 +437,7 @@ internal object TextLayoutManager { @OptIn(UnstableReactNativeAPI::class) private fun buildSpannableFromFragmentsOptimized( assets: AssetManager, + fontWeightAdjustment: Int, fragments: MapBuffer, outputReactTags: IntArray?, textEffectRegistry: TextEffectRegistry?, @@ -552,6 +564,7 @@ internal object TextLayoutManager { fragment.props.fontFeatureSettings, fragment.props.fontFamily, assets, + fontWeightAdjustment, ), start, end, @@ -649,42 +662,51 @@ internal object TextLayoutManager { return spannable } - @OptIn(UnstableReactNativeAPI::class) - fun getOrCreateSpannableForText( - assets: AssetManager, - attributedString: MapBuffer, - reactTextViewManagerCallback: ReactTextViewManagerCallback?, - ): Spannable = - getOrCreateSpannableForText(assets, attributedString, reactTextViewManagerCallback, null) - @OptIn(UnstableReactNativeAPI::class) internal fun getOrCreateSpannableForText( assets: AssetManager, + fontWeightAdjustment: Int, attributedString: MapBuffer, reactTextViewManagerCallback: ReactTextViewManagerCallback?, - textEffectRegistry: TextEffectRegistry?, + textEffectRegistry: TextEffectRegistry? = null, ): Spannable { - var text: Spannable? if (attributedString.contains(AS_KEY_CACHE_ID)) { val cacheId = attributedString.getInt(AS_KEY_CACHE_ID) - text = checkNotNull(tagToSpannableCache[cacheId]) - } else { - text = + val cachedSpannable = checkNotNull(tagToSpannableCache[cacheId]) + if (cachedSpannable.fontWeightAdjustment == fontWeightAdjustment) { + return cachedSpannable.spannable + } + + val text = createSpannableFromAttributedString( assets, + fontWeightAdjustment, attributedString.getMapBuffer(AS_KEY_FRAGMENTS), reactTextViewManagerCallback, null, textEffectRegistry, ) + val baseTextAttributes = + TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES)) + setTextPaintHolderSpan(text, baseTextAttributes, assets, fontWeightAdjustment) + tagToSpannableCache[cacheId] = CachedSpannable(text, fontWeightAdjustment) + return text } - return text + return createSpannableFromAttributedString( + assets, + fontWeightAdjustment, + attributedString.getMapBuffer(AS_KEY_FRAGMENTS), + reactTextViewManagerCallback, + null, + textEffectRegistry, + ) } @OptIn(UnstableReactNativeAPI::class) private fun createSpannableFromAttributedString( assets: AssetManager, + fontWeightAdjustment: Int, fragments: MapBuffer, reactTextViewManagerCallback: ReactTextViewManagerCallback?, outputReactTags: IntArray?, @@ -694,6 +716,7 @@ internal object TextLayoutManager { val spannable = buildSpannableFromFragmentsOptimized( assets, + fontWeightAdjustment, fragments, outputReactTags, textEffectRegistry, @@ -709,7 +732,15 @@ internal object TextLayoutManager { // a new spannable will be wiped out val ops: MutableList = ArrayList() - buildSpannableFromFragments(assets, fragments, sb, ops, outputReactTags, textEffectRegistry) + buildSpannableFromFragments( + assets, + fontWeightAdjustment, + fragments, + sb, + ops, + outputReactTags, + textEffectRegistry, + ) // TODO T31905686: add support for inline Images // While setting the Spans on the final text, we also check whether any of them are images. @@ -825,10 +856,11 @@ internal object TextLayoutManager { * Sets attributes on the TextPaint, used for content outside the Spannable text, like for empty * strings, or newlines after the last trailing character */ - private fun updateTextPaint( + internal fun updateTextPaint( paint: TextPaint, baseTextAttributes: TextAttributeProps, assets: AssetManager, + fontWeightAdjustment: Int, ) { if (baseTextAttributes.fontSize != ReactConstants.UNSET) { paint.textSize = baseTextAttributes.fontSize.toFloat() @@ -847,7 +879,7 @@ internal object TextLayoutManager { baseTextAttributes.fontFamily, assets, ) - paint.setTypeface(typeface) + paint.setTypeface(ReactTypefaceUtils.applyFontWeightAdjustment(typeface, fontWeightAdjustment)) if ( baseTextAttributes.fontStyle != ReactConstants.UNSET && @@ -858,6 +890,8 @@ internal object TextLayoutManager { paint.isFakeBoldText = missingStyle and Typeface.BOLD != 0 paint.textSkewX = if ((missingStyle and Typeface.ITALIC) != 0) -0.25f else 0f } + } else { + paint.setTypeface(ReactTypefaceUtils.applyFontWeightAdjustment(null, fontWeightAdjustment)) } } @@ -868,28 +902,47 @@ internal object TextLayoutManager { private fun scratchPaintWithAttributes( baseTextAttributes: TextAttributeProps, assets: AssetManager, + fontWeightAdjustment: Int, ): TextPaint { val paint = checkNotNull(textPaintInstance.get()) paint.setTypeface(null) paint.textSize = 12f paint.isFakeBoldText = false paint.textSkewX = 0f - updateTextPaint(paint, baseTextAttributes, assets) + updateTextPaint(paint, baseTextAttributes, assets, fontWeightAdjustment) return paint } private fun newPaintWithAttributes( baseTextAttributes: TextAttributeProps, assets: AssetManager, + fontWeightAdjustment: Int, ): TextPaint { val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG) - updateTextPaint(paint, baseTextAttributes, assets) + updateTextPaint(paint, baseTextAttributes, assets, fontWeightAdjustment) return paint } + private fun setTextPaintHolderSpan( + text: Spannable, + baseTextAttributes: TextAttributeProps, + assets: AssetManager, + fontWeightAdjustment: Int, + ) { + text.setSpan( + ReactTextPaintHolderSpan( + newPaintWithAttributes(baseTextAttributes, assets, fontWeightAdjustment) + ), + 0, + text.length, + Spannable.SPAN_INCLUSIVE_INCLUSIVE, + ) + } + @OptIn(UnstableReactNativeAPI::class) private fun createLayoutForMeasurement( assets: AssetManager, + fontWeightAdjustment: Int, attributedString: MapBuffer, paragraphAttributes: MapBuffer, width: Float, @@ -902,6 +955,7 @@ internal object TextLayoutManager { val text = getOrCreateSpannableForText( assets, + fontWeightAdjustment, attributedString, reactTextViewManagerCallback, textEffectRegistry, @@ -913,7 +967,7 @@ internal object TextLayoutManager { } else { val baseTextAttributes = TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES)) - paint = scratchPaintWithAttributes(baseTextAttributes, assets) + paint = scratchPaintWithAttributes(baseTextAttributes, assets, fontWeightAdjustment) } return createLayout( @@ -1020,6 +1074,7 @@ internal object TextLayoutManager { @OptIn(UnstableReactNativeAPI::class) fun createPreparedLayout( assets: AssetManager, + fontWeightAdjustment: Int, attributedString: ReadableMapBuffer, paragraphAttributes: ReadableMapBuffer, width: Float, @@ -1034,6 +1089,7 @@ internal object TextLayoutManager { val text = createSpannableFromAttributedString( assets, + fontWeightAdjustment, fragments, reactTextViewManagerCallback, reactTags, @@ -1044,7 +1100,7 @@ internal object TextLayoutManager { val result = createLayout( text, - newPaintWithAttributes(baseTextAttributes, assets), + newPaintWithAttributes(baseTextAttributes, assets, fontWeightAdjustment), attributedString, paragraphAttributes, width, @@ -1186,6 +1242,7 @@ internal object TextLayoutManager { @OptIn(UnstableReactNativeAPI::class) fun measureText( assets: AssetManager, + fontWeightAdjustment: Int, attributedString: MapBuffer, paragraphAttributes: MapBuffer, width: Float, @@ -1200,6 +1257,7 @@ internal object TextLayoutManager { val layout = createLayoutForMeasurement( assets, + fontWeightAdjustment, attributedString, paragraphAttributes, width, @@ -1459,6 +1517,7 @@ internal object TextLayoutManager { @OptIn(UnstableReactNativeAPI::class) fun measureLines( assetManager: AssetManager, + fontWeightAdjustment: Int, attributedString: MapBuffer, paragraphAttributes: MapBuffer, width: Float, @@ -1469,6 +1528,7 @@ internal object TextLayoutManager { val layout = createLayoutForMeasurement( assetManager, + fontWeightAdjustment, attributedString, paragraphAttributes, width, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CustomStyleSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CustomStyleSpan.kt index dec852214560..29eb30f14470 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CustomStyleSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CustomStyleSpan.kt @@ -33,13 +33,30 @@ internal class CustomStyleSpan( val fontFeatureSettings: String?, val fontFamily: String?, private val assetManager: AssetManager, + private val fontWeightAdjustment: Int = 0, ) : MetricAffectingSpan(), ReactSpan { override fun updateDrawState(ds: TextPaint) { - apply(ds, privateStyle, privateWeight, fontFeatureSettings, fontFamily, assetManager) + apply( + ds, + privateStyle, + privateWeight, + fontFeatureSettings, + fontFamily, + assetManager, + fontWeightAdjustment, + ) } override fun updateMeasureState(paint: TextPaint) { - apply(paint, privateStyle, privateWeight, fontFeatureSettings, fontFamily, assetManager) + apply( + paint, + privateStyle, + privateWeight, + fontFeatureSettings, + fontFamily, + assetManager, + fontWeightAdjustment, + ) } val style: Int @@ -66,12 +83,15 @@ internal class CustomStyleSpan( fontFeatureSettingsParam: String?, family: String?, assetManager: AssetManager, + fontWeightAdjustment: Int, ) { val typeface = ReactTypefaceUtils.applyStyles(paint.typeface, style, weight, family, assetManager) + val adjustedTypeface = + ReactTypefaceUtils.applyFontWeightAdjustment(typeface, fontWeightAdjustment) paint.apply { fontFeatureSettings = fontFeatureSettingsParam - setTypeface(typeface) + setTypeface(adjustedTypeface) isSubpixelText = true isLinearText = true } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt index aae65314509b..0fc4ceee4e86 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -70,7 +70,9 @@ import com.facebook.react.uimanager.style.BorderStyle import com.facebook.react.uimanager.style.LogicalEdge import com.facebook.react.uimanager.style.Overflow import com.facebook.react.views.text.ReactTextUpdate +import com.facebook.react.views.text.ReactTypefaceUtils.applyFontWeightAdjustment import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles +import com.facebook.react.views.text.ReactTypefaceUtils.getFontWeightAdjustment import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight import com.facebook.react.views.text.TextAttributes @@ -849,7 +851,14 @@ public open class ReactEditText public constructor(context: Context) : AppCompat fontFeatureSettings != null ) { workingText.setSpan( - CustomStyleSpan(fontStyle, fontWeight, fontFeatureSettings, fontFamily, context.assets), + CustomStyleSpan( + fontStyle, + fontWeight, + fontFeatureSettings, + fontFamily, + context.assets, + getFontWeightAdjustment(context), + ), 0, workingText.length, spanFlags, @@ -1103,13 +1112,16 @@ public open class ReactEditText public constructor(context: Context) : AppCompat } addSpansFromStyleAttributes(sb) + val fontWeightAdjustment = getFontWeightAdjustment(context) + val textPaint = TextPaint(paint) + textPaint.setTypeface(applyFontWeightAdjustment(textPaint.typeface, fontWeightAdjustment)) sb.setSpan( - ReactTextPaintHolderSpan(TextPaint(paint)), + ReactTextPaintHolderSpan(textPaint), 0, sb.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE, ) - TextLayoutManager.setCachedSpannableForTag(id, sb) + TextLayoutManager.setCachedSpannableForTag(id, fontWeightAdjustment, sb) } public fun setEventDispatcher(eventDispatcher: EventDispatcher?) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt index 9e40dbe5e69d..8ff81dd0be58 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt @@ -36,6 +36,7 @@ import com.facebook.react.bridge.ReactSoftExceptionLogger.logSoftException import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableType import com.facebook.react.common.ReactConstants +import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.common.mapbuffer.MapBuffer import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderColor @@ -69,6 +70,7 @@ import com.facebook.react.views.text.DefaultStyleValuesUtil.getDefaultTextColorH import com.facebook.react.views.text.ReactTextUpdate import com.facebook.react.views.text.ReactTextUpdate.Companion.buildReactTextUpdateFromState import com.facebook.react.views.text.ReactTextViewManagerCallback +import com.facebook.react.views.text.ReactTypefaceUtils.getFontWeightAdjustment import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariant import com.facebook.react.views.text.TextAttributeProps import com.facebook.react.views.text.TextLayoutManager @@ -999,6 +1001,7 @@ public open class ReactTextInputManager public constructor() : return null } + @OptIn(UnstableReactNativeAPI::class) public fun getReactTextUpdate( view: ReactEditText, props: ReactStylesDiffMap, @@ -1016,6 +1019,7 @@ public open class ReactTextInputManager public constructor() : val spanned = TextLayoutManager.getOrCreateSpannableForText( view.context.assets, + getFontWeightAdjustment(view.context), attributedString, reactTextViewManagerCallback, ) 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 new file mode 100644 index 000000000000..1cead9444cbd --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextLayoutManagerFontWeightAdjustmentTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text + +import android.graphics.Typeface +import android.text.SpannableString +import android.text.TextPaint +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.common.mapbuffer.WritableMapBuffer +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.uimanager.ReactStylesDiffMap +import com.facebook.react.views.text.internal.span.CustomStyleSpan +import com.facebook.react.views.text.internal.span.ReactTextPaintHolderSpan +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RuntimeEnvironment +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +@OptIn(UnstableReactNativeAPI::class) +class TextLayoutManagerFontWeightAdjustmentTest { + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(RuntimeEnvironment.getApplication()) + } + + @After + fun tearDown() { + TextLayoutManager.deleteCachedSpannableForTag(CACHE_ID) + DisplayMetricsHolder.setScreenDisplayMetrics(null) + ReactNativeFeatureFlags.dangerouslyReset() + } + + @Test + fun `plain text paint applies Android font weight adjustment`() { + val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG) + val textAttributes = TextAttributeProps.fromReadableMap(ReactStylesDiffMap(JavaOnlyMap())) + + TextLayoutManager.updateTextPaint( + paint, + textAttributes, + RuntimeEnvironment.getApplication().assets, + FONT_WEIGHT_ADJUSTMENT_BOLD_TEXT, + ) + + assertThat(paint.typeface).isNotNull + assertThat(paint.typeface).isNotSameAs(Typeface.DEFAULT) + } + + @Test + fun `plain text paint keeps default typeface unset without font weight adjustment`() { + val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG) + val textAttributes = TextAttributeProps.fromReadableMap(ReactStylesDiffMap(JavaOnlyMap())) + + TextLayoutManager.updateTextPaint( + paint, + textAttributes, + RuntimeEnvironment.getApplication().assets, + 0, + ) + + assertThat(paint.typeface).isNull() + } + + @Test + fun `cached spannable is recreated when Android font weight adjustment changes`() { + val cachedSpannable = SpannableString("A") + cachedSpannable.setSpan( + ReactTextPaintHolderSpan(TextPaint(TextPaint.ANTI_ALIAS_FLAG)), + 0, + cachedSpannable.length, + 0, + ) + TextLayoutManager.setCachedSpannableForTag(CACHE_ID, 0, cachedSpannable) + + val adjustedSpannable = + TextLayoutManager.getOrCreateSpannableForText( + RuntimeEnvironment.getApplication().assets, + FONT_WEIGHT_ADJUSTMENT_BOLD_TEXT, + cachedAttributedString(), + null, + ) + + assertThat(adjustedSpannable).isNotSameAs(cachedSpannable) + val customStyleSpan = + adjustedSpannable.getSpans(0, adjustedSpannable.length, CustomStyleSpan::class.java)[0] + val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG) + + customStyleSpan.updateDrawState(paint) + + assertThat(paint.typeface).isNotSameAs(Typeface.DEFAULT) + + val holderSpan = + adjustedSpannable + .getSpans(0, adjustedSpannable.length, ReactTextPaintHolderSpan::class.java)[0] + assertThat(holderSpan.textPaint.typeface).isNotSameAs(Typeface.DEFAULT) + + val recachedSpannable = + TextLayoutManager.getOrCreateSpannableForText( + RuntimeEnvironment.getApplication().assets, + FONT_WEIGHT_ADJUSTMENT_BOLD_TEXT, + cachedAttributedString(), + null, + ) + + assertThat(recachedSpannable).isSameAs(adjustedSpannable) + } + + private companion object { + const val CACHE_ID = 1001 + const val FONT_WEIGHT_ADJUSTMENT_BOLD_TEXT = 300 + + fun cachedAttributedString(): WritableMapBuffer { + val textAttributes = + WritableMapBuffer().put(TextAttributeProps.TA_KEY_FONT_WEIGHT, "normal").put( + TextAttributeProps.TA_KEY_FONT_SIZE, + 14.0, + ) + val fragment = + WritableMapBuffer() + .put(TextLayoutManager.FR_KEY_STRING, "A") + .put(TextLayoutManager.FR_KEY_REACT_TAG, 1) + .put(TextLayoutManager.FR_KEY_TEXT_ATTRIBUTES, textAttributes) + val fragments = WritableMapBuffer().put(0, fragment) + return WritableMapBuffer() + .put(TextLayoutManager.AS_KEY_CACHE_ID, CACHE_ID) + .put(TextLayoutManager.AS_KEY_FRAGMENTS, fragments) + .put(TextLayoutManager.AS_KEY_BASE_ATTRIBUTES, WritableMapBuffer()) + } + } +}