Skip to content

fix(android): stop cutting off the last line of content-sized text - #1107

Merged
jnowakow merged 2 commits into
mainfrom
fix/android-text-measure-clipping
Nov 2, 2026
Merged

jnowakow merged 2 commits into
mainfrom
fix/android-text-measure-clipping

Conversation

@jnowakow

@jnowakow jnowakow commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

What/Why?

Fixes #1103.

On Android, EnrichedMarkdownText sized to its content (e.g. a chat bubble) could come out a pixel or two narrower than its text, so the last word wrapped into a line the view had no room for and was cut off. Measurement and drawing laid the text out differently:

  • On API 35+, the text view sizes lines by glyph bounds (useBoundsForWidth), so a line ending in italic is wider than its advances. The measuring StaticLayout didn't, in both the commonmark and the github path.
  • On older Android, the measure paint lacked ANTI_ALIAS_FLAG, which changes glyph widths by a few pixels.
  • Without the textBreakStrategy prop, the view broke lines with SIMPLE while measurement used HIGH_QUALITY.

The views now pin useBoundsForWidth and start from the same break strategy, and every measuring layout uses bounds and an anti-aliased paint.

Testing

The Playground preview fills the screen width, so render the text in a content-sized container instead. Before the fix, the final " it" is missing at several font sizes.

{[14, 15, 16, 17, 18].map((fontSize) => (
  <View
    key={fontSize}
    style={{ alignSelf: 'flex-start', maxWidth: '85%', padding: 10, backgroundColor: '#D6E8FF' }}
  >
    <EnrichedMarkdownText
      flavor="github"
      markdown="***both*** then **bold *it* bold** then *it **bold** it*"
      markdownStyle={{ paragraph: { fontSize } }}
    />
  </View>
))}

Check both flavor="github" and flavor="commonmark".

github

Before After
github before github after

commonmark

Before After
commonmark before commonmark after

PR Checklist

  • Code compiles and runs on iOS
  • Code compiles and runs on Android
  • Updated documentation/README if applicable
  • Ran example app to verify changes
  • E2E tests are passing
  • Required E2E tests have been added (if applicable)

Measure text the way the text views lay it out: by glyph bounds on
API 35+, with an anti-aliased paint, and with the same default break
strategy. A view sized to its content no longer comes out a pixel or
two narrow and wraps its last word into a line it has no room for.

Fixes #1103

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

No active review issues were identified for the Android text-measurement changes intended to prevent last-line clipping.

No open issues 🎉

What changed in this PR

Aligns Android text measurement with rendering to prevent the last line from being clipped in content-sized EnrichedMarkdownText views, addressing #1103 for both CommonMark and GitHub flavors.

Changes:

  • Aligns the default line-break strategy and enables glyph-bound widths on API 35+.
  • Enables anti-aliasing for the shared measurement paint, including after resets.
  • Centralizes measurement layout construction and applies bounds-based widths to segment measurement.
File Description
packages/​react-native-enriched-markdown/​android/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​utils/​text/​view/​TextViewSetup.kt Aligns view break strategy and glyph-bound sizing with measurement.
packages/​react-native-enriched-markdown/​android/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​segments/​SegmentHeightMeasurer.kt Enables bounds-based widths for segment layouts.
packages/​react-native-enriched-markdown/​android/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​MeasurementStore.kt Enables anti-aliased measurement and shares consistent layout construction.

Review effort: Balanced

@jnowakow
jnowakow requested a review from maksg October 28, 2026 13:09
@jnowakow
jnowakow marked this pull request as ready for review October 28, 2026 13:09

@maksg maksg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving, looks good 🎉

Some non-blocking notes:
Checked against the Android 15 sources: TextView enables useBoundsForWidth only for apps targeting 35+, the view's flag reaches its DynamicLayout, and getLineWidth is bounds-aware, so the measured width really grows with the overhang. The SIMPLE default is real too: AccessibleMarkdownTextView passes defStyleAttr = 0.

Setting the flag explicitly is the only option, by the way: StaticLayout.Builder.obtain() never resets mUseBoundsForWidth, and builders are pooled and recycled by build(), so an unset builder inherits whatever TextView left behind.

Outside this PR, same mechanism: CellTextView vs the layouts at TableContainerView.kt:473 and :491. Columns are sized to ceil(textWidth), so a cell ending in italic can wrap and lose a line. Follow-up unless you want to fold it in.

Behaviour note: apps targeting below 35 on Android 15+ now also break lines by glyph bounds, so Android screenshot baselines may move. Right call versus pinning false.

// TextView turns bounds-based widths on only for apps targeting API 35+, while
// MeasurementStore can't see the view. Pin it so both sides measure the same way.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
useBoundsForWidth = true

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: the API 35 guard now lives here and in MeasurementStore.applyUseBoundsForWidth. One helper next to BreakStrategyUtils would keep both pins together and spare SegmentHeightMeasurer the MeasurementStore import. Optional.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's a nice improvement! Pushed in f7c5245

@jnowakow

jnowakow commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Outside this PR, same mechanism: CellTextView vs the layouts at TableContainerView.kt:473 and :491. Columns are sized to ceil(textWidth), so a cell ending in italic can wrap and lose a line. Follow-up unless you want to fold it in.

I'll work on tables in separate PR. Thanks for pointing this out!

@jnowakow

jnowakow commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

iOS was not modified in this PR so I'm skipping its unit tests on this PR.

@jnowakow
jnowakow merged commit e2af6eb into main Nov 2, 2026
18 of 20 checks passed
@jnowakow
jnowakow deleted the fix/android-text-measure-clipping branch November 2, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android: EnrichedMarkdownText cuts off its last line when the widest line nearly fills the available width

3 participants