You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Android] Text measured as 2 lines but drawn as 1 (empty line below) when width is a fractional pixel: floor() in AT_MOST vs ceil() in EXACTLY #59174
On Android, a <Text> whose content almost exactly fills its container is measured as two lines but drawn as one, leaving an empty line of space below it.
Root cause (RN 0.86.3, also present on main): TextLayoutManager.createLayout
(and the BoringLayout guard boring.width <= floor(width)).
Yoga's width in dp is converted to px with float32, e.g. 222.85714 dp × 2.1 (density 336) = 467.99997 px. During measurement (AT_MOST) the layout width becomes floor = 467 px, during drawing (EXACTLY) it becomes ceil = 468 px. Text whose desired width falls in (467, 468] px (here 467.17 px) wraps to 2 lines in measurement but fits on 1 line when drawn. The view keeps the 2-line height.
Measured with onTextLayout / onLayout on a Galaxy Tab A9+ (Android 16, density 2.1, font scale 1.1): lines = 1 line, width 222.46 dp; onLayout height = 46.67 dp (two 23.33 dp lines), container width 222.857 dp. Texts one character shorter or longer behave correctly.
textBreakStrategy="simple", flexBasis changes and width: '100%' do not help (measurement path unchanged).
Steps to reproduce
Row: flexDirection: 'row', a flex: 1 column with a <Text> (custom font, fontSize 16, lineHeight 22), and a small trailing element.
Use a string whose width is within 1 px of the column width on a device whose density makes the column width a non-integer pixel value (e.g. density 2.1 or 2.625).
The Text box is two lines tall while one line is drawn.
Suggested fix
Apply a float tolerance before flooring, so measurement and drawing round to the same pixel:
Missing reproducer: We could not detect a reproducible example in your issue report. Reproducers are mandatory and we can accept only one of those as a valid reproducer:
For majority of bugs: send us a Pull Request with the RNTesterPlayground.js edited to reproduce your bug.
I looked into this and tested a fix on an Android emulator. Sharing the results because the cause looks more complex than the floor/ceil difference alone.
Setup: Medium Phone emulator (density 2.625), RNTester Playground. Each row is a flexDirection: 'row' container with a flex: 1 column holding a <Text> (fontSize 16, lineHeight 22) and a 10 dp trailing view. The column width is set to the text's single-line width (from onTextLayout) plus an offset from -0.5 to +0.5 dp in 0.04 dp steps. A row counts as broken when the <Text> box is taller than the lines it reports in onTextLayout. 416 rows in total.
column alignItems: 'stretch'
column alignItems: 'flex-start'
main
17 broken
0 broken
AT_MOST wraps at ceil(width), reported width clamped to width
17 broken
17 broken
What I found:
The stretch cases are a different problem. In all 17 rows the text really does not fit the column. Example: a 376.16 px text in a 375.9 px column. Measuring correctly gives 2 lines. Yoga then rounds the text box up to the pixel grid (377 px), and at that width the text is drawn on 1 line. The box keeps the 2-line height.
Wrapping at ceil(width) in AT_MOST makes this worse. It reports a fractional width to Yoga, so the flex-start columns now hit the same rounding issue. On main, AT_MOST reports an integer width and stays consistent.
I could not reproduce the exact case from the issue (measure with AT_MOST at floor(width), then draw with EXACTLY at ceil(width)). It probably needs the author's real layout. @YigitGok, could you share a minimal component with the font, or a Snack?
So floor(width + 0.01f) may fix the 467.99997 px case from the issue. But it does not cover the general mismatch between the width the text is measured at and the rounded width it is drawn at. A full fix probably needs measurement and drawing to use the same rounded width.
RNTesterPlayground.js used for the scan
importtype{RNTesterModuleExample}from'../../types/RNTesterTypes';import*asReactfrom'react';import{PixelRatio,ScrollView,StyleSheet,Text,View}from'react-native';constSENTENCE='The quick brown fox jumps over the lazy dog while seven wizards quietly box';constSTRINGS=Array.from({length: 8},(_,i)=>SENTENCE.slice(0,20+i*6).trim(),);constDELTAS=Array.from({length: 26},(_,i)=>-0.5+i*0.04);constLINE_HEIGHT=22;functionRow({
text,
width,
atMost,
onResult,}: {text: string,width: number,atMost: boolean,onResult: (broken: boolean)=>void,}){const[height,setHeight]=React.useState(0);const[lines,setLines]=React.useState(0);// Box is taller than the lines the text is drawn with.constbroken=lines>0&&height>lines*LINE_HEIGHT+LINE_HEIGHT/2;React.useEffect(()=>{if(lines>0&&height>0){onResult(broken);}},[broken,lines,height,onResult]);return(<Viewstyle={[styles.row,{width: width+10}]}><Viewstyle={[styles.column,atMost&&styles.atMost]}><Textstyle={[styles.text,broken&&styles.broken]}onLayout={e=>setHeight(e.nativeEvent.layout.height)}onTextLayout={e=>setLines(e.nativeEvent.lines.length)}>{text}</Text></View><Viewstyle={styles.trailing}/></View>);}functionPlayground(){const[widths,setWidths]=React.useState<{[string]: number}>({});const[results,setResults]=React.useState<{[string]: boolean}>({});constready=Object.keys(widths).length===STRINGS.length;constrows=[];if(ready){for(constsofSTRINGS){for(constdofDELTAS){for(constatMostof[false,true]){rows.push({key: `${s}|${d}|${String(atMost)}`,text: s,width: widths[s]+d, atMost});}}}}constbroken=rows.filter(r=>results[r.key]===true);constbrokenAtMost=broken.filter(r=>r.atMost).length;constdone=rows.filter(r=>results[r.key]!=null).length;return(<ScrollViewcontentContainerStyle={styles.container}>{/* Measure each string's single-line width. */}<Viewstyle={styles.measure}pointerEvents="none">{STRINGS.map(s=>(<Textkey={s}style={styles.text}onTextLayout={e=>{constw=e.nativeEvent.lines[0]?.width;if(w!=null){setWidths(prev=>(prev[s]!=null ? prev : {...prev,[s]: w}));}}}>{s}</Text>))}</View><TexttestID="summary"style={styles.summary}>{ready
? `density ${PixelRatio.get()} | checked ${done}/${rows.length} | extra empty line: stretch ${broken.length-brokenAtMost}, flex-start ${brokenAtMost}`
: 'measuring...'}</Text>{/* Broken rows first, so they show up in the screenshot. */}{[...broken, ...rows.filter(r=>results[r.key]!==true)].map(r=>(<Rowkey={r.key}text={r.text}width={r.width}atMost={r.atMost}onResult={b=>setResults(prev=>(prev[r.key]===b ? prev : {...prev,[r.key]: b}))}/>))}</ScrollView>);}conststyles=StyleSheet.create({container: {padding: 10},measure: {position: 'absolute',left: -5000,width: 4000,opacity: 0},summary: {fontSize: 16,fontWeight: 'bold',marginBottom: 8},row: {flexDirection: 'row',marginBottom: 2},column: {flex: 1},atMost: {alignItems: 'flex-start'},trailing: {width: 10,backgroundColor: '#ccc'},text: {fontSize: 16,lineHeight: LINE_HEIGHT,backgroundColor: '#e0f0ff'},broken: {backgroundColor: '#ffb3b3'},});exportdefault{title: 'Playground',name: 'playground',description: 'Test out new features and ideas.',render: (): React.Node=><Playground/>,}asRNTesterModuleExample;
Activity
react-native-bot commentedon Oct 18, 2026
Warning
Missing reproducer: We could not detect a reproducible example in your issue report. Reproducers are mandatory and we can accept only one of those as a valid reproducer:
You can read more about about it on our website: How to report a bug.
hcbylmz commentedon Oct 21, 2026
I looked into this and tested a fix on an Android emulator. Sharing the results because the cause looks more complex than the
floor/ceildifference alone.Setup: Medium Phone emulator (density 2.625), RNTester Playground. Each row is a
flexDirection: 'row'container with aflex: 1column holding a<Text>(fontSize 16, lineHeight 22) and a 10 dp trailing view. The column width is set to the text's single-line width (fromonTextLayout) plus an offset from -0.5 to +0.5 dp in 0.04 dp steps. A row counts as broken when the<Text>box is taller than the lines it reports inonTextLayout. 416 rows in total.alignItems: 'stretch'alignItems: 'flex-start'mainAT_MOSTwraps atceil(width), reported width clamped towidthWhat I found:
stretchcases are a different problem. In all 17 rows the text really does not fit the column. Example: a 376.16 px text in a 375.9 px column. Measuring correctly gives 2 lines. Yoga then rounds the text box up to the pixel grid (377 px), and at that width the text is drawn on 1 line. The box keeps the 2-line height.ceil(width)inAT_MOSTmakes this worse. It reports a fractional width to Yoga, so theflex-startcolumns now hit the same rounding issue. Onmain,AT_MOSTreports an integer width and stays consistent.AT_MOSTatfloor(width), then draw withEXACTLYatceil(width)). It probably needs the author's real layout. @YigitGok, could you share a minimal component with the font, or a Snack?So
floor(width + 0.01f)may fix the 467.99997 px case from the issue. But it does not cover the general mismatch between the width the text is measured at and the rounded width it is drawn at. A full fix probably needs measurement and drawing to use the same rounded width.RNTesterPlayground.js used for the scan