Skip to content

Commit 3ee9e05

Browse files
authored
fix: refresh cached spannables for font weight changes
1 parent 63c34a8 commit 3ee9e05

3 files changed

Lines changed: 96 additions & 6 deletions

File tree

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

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,12 @@ internal object TextLayoutManager {
109109

110110
private const val DEFAULT_ADJUST_FONT_SIZE_TO_FIT = false
111111

112-
private val tagToSpannableCache = ConcurrentHashMap<Int, Spannable>()
112+
private data class CachedSpannable(
113+
val spannable: Spannable,
114+
val fontWeightAdjustment: Int,
115+
)
116+
117+
private val tagToSpannableCache = ConcurrentHashMap<Int, CachedSpannable>()
113118

114119
// Lazily cached Method for StaticLayout.Builder.setUseBoundsForWidth (API 35+).
115120
// Reflection is needed because some internal targets compile against an SDK older than 35.
@@ -123,8 +128,15 @@ internal object TextLayoutManager {
123128
}
124129
}
125130

126-
fun setCachedSpannableForTag(reactTag: Int, sp: Spannable): Unit {
127-
tagToSpannableCache[reactTag] = sp
131+
fun setCachedSpannableForTag(reactTag: Int, sp: Spannable): Unit =
132+
setCachedSpannableForTag(reactTag, 0, sp)
133+
134+
fun setCachedSpannableForTag(
135+
reactTag: Int,
136+
fontWeightAdjustment: Int,
137+
sp: Spannable,
138+
): Unit {
139+
tagToSpannableCache[reactTag] = CachedSpannable(sp, fontWeightAdjustment)
128140
}
129141

130142
fun deleteCachedSpannableForTag(reactTag: Int): Unit {
@@ -702,7 +714,20 @@ internal object TextLayoutManager {
702714
var text: Spannable?
703715
if (attributedString.contains(AS_KEY_CACHE_ID)) {
704716
val cacheId = attributedString.getInt(AS_KEY_CACHE_ID)
705-
text = checkNotNull(tagToSpannableCache[cacheId])
717+
val cachedSpannable = checkNotNull(tagToSpannableCache[cacheId])
718+
text =
719+
if (cachedSpannable.fontWeightAdjustment == fontWeightAdjustment) {
720+
cachedSpannable.spannable
721+
} else {
722+
createSpannableFromAttributedString(
723+
assets,
724+
fontWeightAdjustment,
725+
attributedString.getMapBuffer(AS_KEY_FRAGMENTS),
726+
reactTextViewManagerCallback,
727+
null,
728+
textEffectRegistry,
729+
)
730+
}
706731
} else {
707732
text =
708733
createSpannableFromAttributedString(
@@ -962,7 +987,15 @@ internal object TextLayoutManager {
962987

963988
val paint: TextPaint
964989
if (attributedString.contains(AS_KEY_CACHE_ID)) {
965-
paint = text.getSpans(0, 0, ReactTextPaintHolderSpan::class.java)[0].textPaint
990+
val textPaintHolderSpans = text.getSpans(0, 0, ReactTextPaintHolderSpan::class.java)
991+
paint =
992+
if (textPaintHolderSpans.isNotEmpty()) {
993+
textPaintHolderSpans[0].textPaint
994+
} else {
995+
val baseTextAttributes =
996+
TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES))
997+
scratchPaintWithAttributes(baseTextAttributes, assets, fontWeightAdjustment)
998+
}
966999
} else {
9671000
val baseTextAttributes =
9681001
TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1117,7 +1117,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
11171117
sb.length,
11181118
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
11191119
)
1120-
TextLayoutManager.setCachedSpannableForTag(id, sb)
1120+
TextLayoutManager.setCachedSpannableForTag(id, getFontWeightAdjustment(context), sb)
11211121
}
11221122

11231123
public fun setEventDispatcher(eventDispatcher: EventDispatcher?) {

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextLayoutManagerFontWeightAdjustmentTest.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@
88
package com.facebook.react.views.text
99

1010
import android.content.res.AssetManager
11+
import android.graphics.Typeface
12+
import android.text.SpannableString
1113
import android.text.TextPaint
1214
import com.facebook.react.bridge.JavaOnlyMap
15+
import com.facebook.react.common.mapbuffer.WritableMapBuffer
16+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
17+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
1318
import com.facebook.react.uimanager.DisplayMetricsHolder
1419
import com.facebook.react.uimanager.ReactStylesDiffMap
20+
import com.facebook.react.views.text.internal.span.CustomStyleSpan
21+
import com.facebook.react.views.text.internal.span.ReactTextPaintHolderSpan
1522
import org.assertj.core.api.Assertions.assertThat
1623
import org.junit.After
1724
import org.junit.Before
@@ -27,12 +34,14 @@ class TextLayoutManagerFontWeightAdjustmentTest {
2734

2835
@Before
2936
fun setUp() {
37+
ReactNativeFeatureFlagsForTests.setUp()
3038
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(RuntimeEnvironment.getApplication())
3139
}
3240

3341
@After
3442
fun tearDown() {
3543
DisplayMetricsHolder.setScreenDisplayMetrics(null)
44+
ReactNativeFeatureFlags.dangerouslyReset()
3645
}
3746

3847
@Test
@@ -65,6 +74,35 @@ class TextLayoutManagerFontWeightAdjustmentTest {
6574
assertThat(paint.typeface).isNull()
6675
}
6776

77+
@Test
78+
fun `cached spannable is recreated when Android font weight adjustment changes`() {
79+
val cachedSpannable = SpannableString("A")
80+
cachedSpannable.setSpan(
81+
ReactTextPaintHolderSpan(TextPaint(TextPaint.ANTI_ALIAS_FLAG)),
82+
0,
83+
cachedSpannable.length,
84+
0,
85+
)
86+
TextLayoutManager.setCachedSpannableForTag(CACHE_ID, 0, cachedSpannable)
87+
88+
val adjustedSpannable =
89+
TextLayoutManager.getOrCreateSpannableForText(
90+
RuntimeEnvironment.getApplication().assets,
91+
FONT_WEIGHT_ADJUSTMENT_BOLD_TEXT,
92+
cachedAttributedString(),
93+
null,
94+
)
95+
96+
assertThat(adjustedSpannable).isNotSameAs(cachedSpannable)
97+
val customStyleSpan =
98+
adjustedSpannable.getSpans(0, adjustedSpannable.length, CustomStyleSpan::class.java)[0]
99+
val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG)
100+
101+
customStyleSpan.updateDrawState(paint)
102+
103+
assertThat(paint.typeface).isNotSameAs(Typeface.DEFAULT)
104+
}
105+
68106
private fun invokeUpdateTextPaint(
69107
paint: TextPaint,
70108
textAttributes: TextAttributeProps,
@@ -87,6 +125,25 @@ class TextLayoutManagerFontWeightAdjustmentTest {
87125
}
88126

89127
private companion object {
128+
const val CACHE_ID = 1001
90129
const val FONT_WEIGHT_ADJUSTMENT_BOLD_TEXT = 300
130+
131+
fun cachedAttributedString(): WritableMapBuffer {
132+
val textAttributes =
133+
WritableMapBuffer().put(TextAttributeProps.TA_KEY_FONT_WEIGHT, "normal").put(
134+
TextAttributeProps.TA_KEY_FONT_SIZE,
135+
14.0,
136+
)
137+
val fragment =
138+
WritableMapBuffer()
139+
.put(TextLayoutManager.FR_KEY_STRING, "A")
140+
.put(TextLayoutManager.FR_KEY_REACT_TAG, 1)
141+
.put(TextLayoutManager.FR_KEY_TEXT_ATTRIBUTES, textAttributes)
142+
val fragments = WritableMapBuffer().put(0, fragment)
143+
return WritableMapBuffer()
144+
.put(TextLayoutManager.AS_KEY_CACHE_ID, CACHE_ID)
145+
.put(TextLayoutManager.AS_KEY_FRAGMENTS, fragments)
146+
.put(TextLayoutManager.AS_KEY_BASE_ATTRIBUTES, WritableMapBuffer())
147+
}
91148
}
92149
}

0 commit comments

Comments
 (0)