Skip to content

fix(android): measure spacer lines the same height they are drawn - #1062

Open
ryuhzk wants to merge 1 commit into
software-mansion:mainfrom
ryuhzk:fix/android-spacer-line-height
Open

ryuhzk wants to merge 1 commit into
software-mansion:mainfrom
ryuhzk:fix/android-spacer-line-height

Conversation

@ryuhzk

@ryuhzk ryuhzk commented Oct 23, 2026

Copy link
Copy Markdown

Fixes #1060

What/Why?

A spacer line (a lone "\n" carrying a MarginBottomSpan) gets LineHeightSpan from the surrounding text as well. Both spans rewrite the same FontMetricsInt in chooseHeight, so the spacer's height depends on which one runs last. Android does not call them in the same order for the shadow node's StaticLayout and for the TextView's own layout. Yoga then sizes the view with marginBottom-tall spacers while the TextView draws them almost a full line tall, and the last line is drawn below the view's bottom edge. The details are in #1060.

LineHeightSpan.chooseHeight now returns early for spacer lines, so MarginBottomSpan alone decides their height and span order no longer matters. Other lines behave as before, including the existing early return for lines already taller than lineHeight (#643).

The change is limited to the React Native package. packages/enriched-markdown-android/ui has an identical LineHeightSpan, but I have not checked whether its layout paths ever call the two spans in different orders, so I left it unchanged.

Testing

  • New Robolectric test SpacerLineHeightTest runs both spans over a spacer line in both orders and checks that the spacer is marginBottom tall each time. It also checks that ordinary lines still get lineHeight. On main, the order-swap case fails: the spacer comes out 60 px instead of 10 px. With this change both tests pass.
  • I could not run :react-native-enriched-markdown:testDebugUnitTest the usual way, because it needs the example app's Gradle build, and that needs node_modules, which I did not install. Instead I compiled the two span files and the test in a minimal standalone Android library project: AGP 9.2.1, Kotlin 2.2.10, react-android 0.88.0, Robolectric 4.14.1, offline. Result: SpacerLineHeightTest, 2 tests, 0 failures. I used sdk = [28] in the test config, as EmptyListAnchorFormattingTest does, because that Robolectric runtime was already available offline here. I am happy to switch it to 30 to match the other tests.
  • I did not run ktlint for the same reason; I followed the formatting of the surrounding files.
  • I did not run the example app or Maestro. We have shipped this change as a patch on 1.1.1 in our app's Android production build. With it, the measured and drawn heights of long messages with headings and code blocks match.

PR Checklist

  • Code compiles and runs on iOS (Android-only change)
  • Code compiles and runs on Android (compiled and unit-tested in a standalone harness, see above; not run in the example app)
  • Updated documentation/README if applicable (not applicable)
  • Ran example app to verify changes
  • E2E tests are passing (not run)
  • Required E2E tests have been added (if applicable)

MarginBottomSpan makes a lone "\n" spacer line exactly marginBottom tall,
and LineHeightSpan grows any line with spare leading up to lineHeight. Both
edit the line's FontMetricsInt in place, so the spacer's height depends on
which span runs last. Android does not call them in the same order for the
shadow node's StaticLayout and for the TextView's own layout, so Yoga can
size the view from short spacers while the TextView draws tall ones, and
the last line ends up below the view's bottom edge.

LineHeightSpan now leaves spacer lines alone, which makes the result
independent of span order.

This branch has not been deployed

No deployments
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] Spacer lines are measured shorter than they are drawn, so the last line overflows the view

1 participant