Repository navigation
Conversation
6 tasks done
setLineHeight passed `lineHeight - textSize` to EditText.setLineSpacing, mixing SP with pixels, so lines overlapped on high-density screens. It also left Yoga measurement unaware of line height, so the input grew slower than its content. Follow React Native's ReactEditText instead: keep fontSize and lineHeight in SP on TextAttributes (so allowFontScaling applies) and apply the line height as InputCssLineHeightSpan, a copy of RN's CustomLineHeightSpan. Headings derive their line height from the body's extra leading. InputMeasurementStore measures a snapshot of the editor's paint with the same line height and block spans, and with the hint when the text is empty. Yoga is now re-measured after every text change and formatting pass, so toggling a heading or list updates the height. InputListItemSpacingSpan implements UpdateLayout so EditText rebuilds the lines it affects.
The playground toggle switches the input between the default line height and 36, to check rendering and auto-grow with headings and lists. Co-authored-by: Cursor <cursoragent@cursor.com>
hsource
force-pushed
the
fix/android-line-height-measurement
branch
from
September 23, 2026 20:40
844c5b7 to
3ad7cce
Compare
6 tasks done
Author
|
Moved upstream: software-mansion#885 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note: this PR has been manually reviewed and is a version of code that's been part of the Wanderlog (wanderlog.com) mobile apps for the past 3 weeks
What/Why?
Fixes software-mansion#615 on Android.
style.lineHeightonEnrichedMarkdownTextInputis broken in two ways:setLineHeightpassedlineHeight - view.textSizetoEditText.setLineSpacing. That subtracts a pixel text size from an SP line height (for example24 - 48 = -24on a 3x screen), so lines overlap.InputMeasurementStoremeasured plain text with a bareTextPaintand no line spacing. Yoga under-measured the input, so it grew more slowly than its content and scrolled internally.This follows how React Native's own
ReactEditTexthandleslineHeight. It does not adjustsetLineSpacing:TextAttributes:fontSizeandlineHeightare stored in SP on aTextAttributes, soallowFontScalingandeffectiveLineHeightbehave likeTextInput(ReactEditText L332-L334, L856-L858).CustomLineHeightSpaninaddSpansFromStyleAttributes, so markdown spans such as headings stay on top. React Native also adds font size, color and typeface spans there, because itsTextLayoutManagermeasures with a shared paint. The editor draws with its own paint and the measurement uses a snapshot of it, so line height is the only base style that needs a span.InputCssLineHeightSpan: copiesCustomLineHeightSpan.chooseHeight, which splits the extra space above and below each line like CSS. It implementsUpdateLayout, soEditTextrebuilds its layout when the span is re-applied.lineHeight - fontSize). A large heading doesn't get squeezed into the body line height, and it keeps the same spacing rhythm.InputMeasurementStorelays out a snapshot of the editor's paint with the same line-height span and block formatting. When the buffer is empty it measures the placeholder, likeTextInputdoes. The first measurement before the view exists (initialMeasure) readsfontSize,lineHeightandmarkdownStylefrom props, and skips props that are missing. ChangingfontSizeorlineHeightat runtime reapplies formatting, so derived heading line heights update too.applyFormatting(), not only when the text changes. Toggling a heading or list now updates the input height.InputListItemSpacingSpanimplementsUpdateLayoutfor the same reason as the line-height span.Compared with software-mansion#664: that PR keeps
setLineSpacingand converts withtoPixelFromDIP. That treatslineHeightas DP, so it ignores font scaling, and it adds all the extra space below each line. It also leaves headings at the body line height. This PR copies React Native's own implementation, so the input matches a plainTextInputwith the same style.The playground gets an LH toggle, which switches between the default line height and 36, plus an Android Maestro flow.
Testing
Summary:
yarn typecheck, eslint, prettier, ktlint, AndroidcompileDebugKotlin, and an example app build on a Pixel 6a (Android 16).Real-world testing
This has been running in production in Wanderlog's rich text editor for the past 3 weeks and seems to work well.
Test results
line_height_input_android_test.yamlpasses on a Pixel 6a. Its screenshot step was run in capture mode, because the golden has to come from the CI Pixel 9 emulator.Left: default line height. Right: after tapping LH 36. Body and list lines are evenly spaced and don't overlap, and the editor grows until the playground's
maxHeightof 200, then scrolls to keep the caret visible. That scroll is why the heading is off the top in the right-hand frame.Scrolled back to the top at LH 36: the heading uses its derived line height (heading font size plus the body's extra leading).
Full flow recording:
Manual testing
Setup:
yarn && yarn prepare yarn react-native-example start yarn react-native-example androidIn the example app, open Playground:
TextInput.TextInputwith the samefontSizeandlineHeightnext to the editor. Expected: the lines match up.Automated flow (screenshot-based; capture the golden first):
PR Checklist