Description
On Android, a <Text numberOfLines={n}> whose text ellipsizes can be drawn wider than its view. The last glyph or
the ellipsis dots at the far edge (right in LTR, left in RTL) are cut. It happens whenever the text's font has a wider
«…» than Roboto, or the system font scale is above 1.0. With a common Arabic/Latin family (IBM Plex Sans Arabic) we
measured it on 28–51 of every 96 lines forced to ellipsize, at font scales 1.0, 1.3 and 2.0.
Cause (read from the 0.86.3 source and measured)
With enablePreparedTextLayout off (the 0.86 default), <Text> is drawn by ReactTextView, a TextView that lays
out and ellipsizes with its own StaticLayout. Android reserves room for the ellipsis with the layout's paint,
paint.measureText("…") (StaticLayout.calculateEllipsis), and then draws the kept text and «…» with the spans.
ReactTextView.applyTextAttributes() sets that paint's text size from the fontSize prop with
PixelUtil.toPixelFromDIP (no font scale), and nothing sets its typeface, so it stays Roboto. The spans carry the
real family and the scaled size. So the room reserved is a Roboto «…» at 1.0×, and the «…» drawn is the family's at up
to maxFontSizeMultiplier×. When the kept text uses the difference, the line overruns and TextView.onDraw clips it at
the padding edge.
For example, 16 dp SemiBold at font scale 1.3 on a 2.625 density device: reserved 28.1 px (Roboto «…» at 42 px), drawn
~47 px (IBM Plex «…» at 54–55 px). The line can overrun by ~19 px.
Fabric's own layout for the same text (the one onTextLayout reports) is built with the right paint and never
overruns. So onTextLayout describes a line that is not the one on screen.
Steps to reproduce
- Load a custom font whose U+2026 is wider than Roboto's 0.669 em (e.g. IBM Plex Sans Arabic, 0.80–0.90 em).
- Render
<Text numberOfLines={1} style={{ fontFamily: 'IBMPlexSansArabic', fontSize: 16, width: 200 }}> with text
long enough to ellipsize. Sweep the width by a few px, or the text by one character.
- Some widths draw a cut last dot, or a cut last glyph before the «…». Font scale 1.3 makes it worse, and
right-to-left text shows it at the left edge.
Expected
The ellipsized line fits its view, as on iOS and as Fabric's own layout says.
Suggested fix
Give ReactTextView's paint the base text attributes' typeface and scaled size (the same values
TextLayoutManager.updateTextPaint gives Fabric's paint), or make enablePreparedTextLayout the default.
Workaround we use
Nest the text: an outer <Text> whose fontSize is the real size × 3, around an inner <Text> at the real size. The
outer size only feeds ReactTextView's paint, so the reserved «…» covers the drawn one. The factor 3 also covers a
second, Android-framework effect: an Arabic letter cut mid-word takes its final form beside the «…», which
calculateEllipsis never measures (a medial ب is 310 units in this font, a final one 919). That part is not React
Native's and would need its own AOSP report.
Environment
React Native 0.86.3 (New Architecture, enablePreparedTextLayout default false), Expo SDK 57, Android 16 emulator
(Pixel 9, API 36), density 2.625.
Description
On Android, a
<Text numberOfLines={n}>whose text ellipsizes can be drawn wider than its view. The last glyph orthe ellipsis dots at the far edge (right in LTR, left in RTL) are cut. It happens whenever the text's font has a wider
«…» than Roboto, or the system font scale is above 1.0. With a common Arabic/Latin family (IBM Plex Sans Arabic) we
measured it on 28–51 of every 96 lines forced to ellipsize, at font scales 1.0, 1.3 and 2.0.
Cause (read from the 0.86.3 source and measured)
With
enablePreparedTextLayoutoff (the 0.86 default),<Text>is drawn byReactTextView, aTextViewthat laysout and ellipsizes with its own
StaticLayout. Android reserves room for the ellipsis with the layout's paint,paint.measureText("…")(StaticLayout.calculateEllipsis), and then draws the kept text and «…» with the spans.ReactTextView.applyTextAttributes()sets that paint's text size from thefontSizeprop withPixelUtil.toPixelFromDIP(no font scale), and nothing sets its typeface, so it stays Roboto. The spans carry thereal family and the scaled size. So the room reserved is a Roboto «…» at 1.0×, and the «…» drawn is the family's at up
to
maxFontSizeMultiplier×. When the kept text uses the difference, the line overruns andTextView.onDrawclips it atthe padding edge.
For example, 16 dp SemiBold at font scale 1.3 on a 2.625 density device: reserved 28.1 px (Roboto «…» at 42 px), drawn
~47 px (IBM Plex «…» at 54–55 px). The line can overrun by ~19 px.
Fabric's own layout for the same text (the one
onTextLayoutreports) is built with the right paint and neveroverruns. So
onTextLayoutdescribes a line that is not the one on screen.Steps to reproduce
<Text numberOfLines={1} style={{ fontFamily: 'IBMPlexSansArabic', fontSize: 16, width: 200 }}>with textlong enough to ellipsize. Sweep the width by a few px, or the text by one character.
right-to-left text shows it at the left edge.
Expected
The ellipsized line fits its view, as on iOS and as Fabric's own layout says.
Suggested fix
Give
ReactTextView's paint the base text attributes' typeface and scaled size (the same valuesTextLayoutManager.updateTextPaintgives Fabric's paint), or makeenablePreparedTextLayoutthe default.Workaround we use
Nest the text: an outer
<Text>whosefontSizeis the real size × 3, around an inner<Text>at the real size. Theouter size only feeds
ReactTextView's paint, so the reserved «…» covers the drawn one. The factor 3 also covers asecond, Android-framework effect: an Arabic letter cut mid-word takes its final form beside the «…», which
calculateEllipsisnever measures (a medial ب is 310 units in this font, a final one 919). That part is not ReactNative's and would need its own AOSP report.
Environment
React Native 0.86.3 (New Architecture,
enablePreparedTextLayoutdefault false), Expo SDK 57, Android 16 emulator(Pixel 9, API 36), density 2.625.