Skip to content

Fix Android Text clipping with Bold text setting#57118

Open
TorinAsakura wants to merge 11 commits into
react:mainfrom
torin-asakura:fix/android-bold-text-measurement
Open

Fix Android Text clipping with Bold text setting#57118
TorinAsakura wants to merge 11 commits into
react:mainfrom
torin-asakura:fix/android-bold-text-measurement

Conversation

@TorinAsakura

@TorinAsakura TorinAsakura commented Jun 8, 2026

Copy link
Copy Markdown

Summary:

Fixes #54931.
Close torin-asakura/workspace#119.

Android's Bold text accessibility setting updates Configuration.fontWeightAdjustment, which makes platform TextView draw text with an adjusted typeface. React Native was measuring Text without that adjustment, so Yoga could allocate a row item width that was too small and the rendered text was clipped.

This passes the current font weight adjustment through Fabric text measurement, prepared text layout, mounted Text spans, and TextInput spans, and applies it to the TextPaint/CustomStyleSpan typeface used for measurement and drawing.

Changelog:

[ANDROID] [FIXED] - Account for Android Bold text font weight adjustment when measuring Text.

Test Plan:

  • ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests com.facebook.react.views.text.TextLayoutManagerFontWeightAdjustmentTest -Preact.internal.useHermesStable=true
  • ./gradlew :packages:react-native:ReactAndroid:ktfmtCheck -Preact.internal.useHermesStable=true

Reproduced locally before the fix with https://github.com/Lego4m/reproducer-react-native-android-bold-text on Android 16 AVD by toggling font_weight_adjustment between 0 and 300.

Screenshots from #54931:

Bold text off Bold text on
Bold text off Bold text on

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 8, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Jun 8, 2026
@meta-codesync

meta-codesync Bot commented Jun 10, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D108136049.

textEffectRegistry: TextEffectRegistry?,
): Spannable {
var text: Spannable?
if (attributedString.contains(AS_KEY_CACHE_ID)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new fontWeightAdjustment parameter is only applied when creating a fresh spannable. When AS_KEY_CACHE_ID is present, the previously cached spannable is returned (lines 704-716) without revalidation — carrying stale CustomStyleSpan weights from before the Bold Text setting changed.

@TorinAsakura TorinAsakura Jun 11, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

@Test
fun `plain text paint applies Android font weight adjustment`() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test only asserts paint.typeface is non-null. If applyFontWeightAdjustment returned any non-null typeface without applying the weight adjustment (e.g., always Typeface.DEFAULT at weight 400), the test would still pass while the production bug persisted.

@TorinAsakura TorinAsakura Jun 11, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +676 to +697
@OptIn(UnstableReactNativeAPI::class)
fun getOrCreateSpannableForText(
assets: AssetManager,
fontWeightAdjustment: Int,
attributedString: MapBuffer,
reactTextViewManagerCallback: ReactTextViewManagerCallback?,
): Spannable =
getOrCreateSpannableForText(
assets,
fontWeightAdjustment,
attributedString,
reactTextViewManagerCallback,
null,
)

@OptIn(UnstableReactNativeAPI::class)
internal fun getOrCreateSpannableForText(
assets: AssetManager,
attributedString: MapBuffer,
reactTextViewManagerCallback: ReactTextViewManagerCallback?,
textEffectRegistry: TextEffectRegistry?,
): Spannable =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are internal functions, so we don't need to keep all the overloads for backwards compatibility.

@TorinAsakura TorinAsakura Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 717 to 743

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid duplication, use an early return

Suggested change
val cachedSpannable = checkNotNull(tagToSpannableCache[cacheId])
if (cachedSpannable.fontWeightAdjustment == fontWeightAdjustment) {
return cachedSpannable.spannable
}
return createSpannableFromAttributedString(
assets,
fontWeightAdjustment,
attributedString.getMapBuffer(AS_KEY_FRAGMENTS),
reactTextViewManagerCallback,
null,
textEffectRegistry,
)

@TorinAsakura TorinAsakura Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +722 to +729
createSpannableFromAttributedString(
assets,
fontWeightAdjustment,
attributedString.getMapBuffer(AS_KEY_FRAGMENTS),
reactTextViewManagerCallback,
null,
textEffectRegistry,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we write back to the cache?

@TorinAsakura TorinAsakura Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (attributedString.contains(AS_KEY_CACHE_ID)) {
paint = text.getSpans(0, 0, ReactTextPaintHolderSpan::class.java)[0].textPaint
val textPaintHolderSpans = text.getSpans(0, 0, ReactTextPaintHolderSpan::class.java)
paint =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the change here? Recreating paint here may have a performance impact - why can't we apply the adjustment when creating the ReactTextPaintHolderSpan?

@TorinAsakura TorinAsakura Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +23 to +24
private const val FONT_WEIGHT_MIN = 1
private const val FONT_WEIGHT_MAX = 1000

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use FontStyle.FONT_WEIGHT_MIN/MAX

@TorinAsakura TorinAsakura Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.java
.getDeclaredMethod(
"updateTextPaint",
TextPaint::class.java,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use reflection, mark the method internal instead

@TorinAsakura TorinAsakura Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reactTextViewManagerCallback: ReactTextViewManagerCallback?,
attachmentsPositions: FloatArray?,
textEffectRegistry: TextEffectRegistry? = null,
): Long =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internal method - don't add overloads

@TorinAsakura TorinAsakura Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TorinAsakura TorinAsakura force-pushed the fix/android-bold-text-measurement branch from 807447e to 97b3430 Compare June 12, 2026 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Text is cut off when inside a View with flexDirection: 'row' and Android “Bold text” setting is enabled

2 participants