Android Text Clipping
Recently I had to understand how Android interprets a font’s vertical metrics, and when there will be or will not be clipping in a text box. And the answer is: “it depends”.
Boy, does it depend.
First, it depends on what technology stack is being used. Some Android applications are written in Flutter - this is Google’s new cross-platform mobile (and web) development framework. The majority of apps, however, are written in the “classic” Android technologies, either in Java or Kotlin, and based on a framework called Views. More recently, Jetpack Compose is a framework which sits on top of Views and makes app development a bit easier.
So here’s our first “it depends”: Flutter-based or Views-based? Let’s get Flutter out of the way first because it’s an easy one: In text, Flutter is rendered with completely different code to Views and Compose. This is actually a good thing because as far as I can tell, text doesn’t clip to any vertical metrics in Flutter. Go as tall or as deep as you like!
Now comes our second “it depends”: Compose or Views? Compose clipping depends a little on the widgets in question. Let me just define some quick terminology here because it’ll be useful later:
- The font ascent is the value of
sTypoAscenderin the font’sOS/2table if the fsSelection bit 7 (USE_TYPO_METRICS) bit is set and the value ofascenderin thehheatable if it isn’t. - The font descent is the value of
sTypoDescenderin the font’sOS/2table ifUSE_TYPO_METRICSis set and the value ofdescenderin thehheatable if it isn’t.
So: A Compose OutlinedTextField clips text to the font ascent and descent. A Compose Text does not clip, but its line height is determined by the font ascent and descent. A Compose OutlinedButton clips to a little above and below the ascent and descent. (I’m not sure how much “a little” is.)
All right, into Views. An Android TextView has a number of flags which control its behaviour. We will consider two for now (it will get worse, I promise): includeFontPadding and elegantTextHeight.
- If
elegantTextHeightis set to false andincludeFontPaddingis set to false, text is clipped to font ascent and descent. Phew. - If
elegantTextHeightif false andincludeFontPaddingistrue, then the line height is set (and text is clipped) to either the font ascent, or the heighestyMaxvalue of theglyftable bounding box of all the glyphs in the run (not the shaped position of the glyph!), whichever is the higher. And likewise, the font depth is set to the minimum of the font descender and theglyftableyMinvalue of the glyphs in the run.
Are you ready for this?
If elegantTextHeight is set to true then the vertical metrics in the font are completely ignored and replaced by hard-coded constants deep in the Android core graphics library. With the effect that:
- If
elegantTextHeightis true, andincludeFontPaddingis false, text is clipped to1900/2048multiplied by the font’s UPM at the top and-500/2048multiplied by the font’s UPM at the bottom. - If
elegantTextHeightis true, andincludeFontPaddingis true, text is clipped to2500/2048multiplied by the font’s UPM at the top and-1000/2048multiplied by the font’s UPM at the bottom.
Oh, and which of these flags is on or off by default depends on Android (and Comppose, if your app is using that) version. In Android 15, elegantTextHeight was turned on by default. In Compose 1.2.0-alpha05, includeFontPadding was turned off by default for Compose widgets; in 1.2.0-beta01, it was turned on by default.
All right, now the kicker.
All that I have told you is true for an individual font. But if you are using multiple fonts in the context of a fallback stack, which you usually are, which set of metrics are used depends entirely on the value of another flag: fallbackLineSpacing.
- If
fallbackLineSpacingis false, the algorithm above is used for the top font in the stack, regardless of which font is used to render glyphs in the run. - If
fallbackLineSpacingis true, the algorithm above is used for the used font in the stack.
Apparently fallbackLineSpacing is true by default.
I made a custom font and a bunch of Android apps (and spent much too long reading Android’s source code) to discover all this…
