Meaning of top, ascent, baseline, descent, bottom, and leading in Android's FontMetrics
Let's first review what the documentation says:
- Top - The maximum distance above the baseline for the tallest glyph in the font at a given text size.
- Ascent - The recommended distance above the baseline for singled spaced text.
- Descent - The recommended distance below the baseline for singled spaced text.
- Bottom - The maximum distance below the baseline for the lowest glyph in the font at a given text size.
- Leading - The recommended additional space to add between lines of text.
Note that the Baseline is what the first four are measured from. It is line which forms the base that the text sits on, even though some characters (like g, y, j, etc.) might have parts that go below the line. It is comparable to the lines you write on in a lined notebook.
Here is a picture to help visualize these things:
Remember that when drawing on a canvas in Java and Android, going down is an increase in y and going up is a decrease in y. That means that FontMetrics' top
and ascent
are negative numbers since they are measured from the baseline (while descent and bottom are positive numbers). Thus, to get the distance from top
to bottom
you would need to do (bottom
- top
).
The leading is the distance between the bottom of one line and the top of the next line. In the picture above, it is the space between the orange of Line 1 and the purple of Line 2. As @MajorTom noted below, in typography the term is more properly defined as "the distance between the baselines of successive lines of type."* However, Android seems to use the term in the more historical sense. The word (pronounced "ledding") comes from the lead strip that the old typesetters used to put between lines of type. It was basically just a way to adjust the line spacing. In Android I've never actually seen the leading be anything other than 0
and I haven't seen it used for anything in the source code. (Correct me if you know where it is used to calculate anything.) You can change the line spacing in a TextView
with setLineSpacing
in code or android:lineSpacingExtra
and android:lineSpacingMultiplier
in xml. These methods, however, do not make use of or modify the leading.
Check out these links for more information:
- Precise Android Text Drawing
- Font Metrics in Java (and Android)
- Layout documentation
- Java: FontMetrics ascent incorrect?
- FontMetrics not correct when run on android device. Simulator fine
- Java Font Metrics (Java doesn't seem to use
top
andbottom
)
Explore more
In order to explore Font Metrics more, I made a simple project.
Rather than listing all the code here. I added the project to GitHub. You can either clone the project, or copy the following files into a new project.
- FontMetricsView.java (a custom view)
- MainActivity.java
- activity_main.xml
Do letters ever go above top
or below bottom
?
Not usually, but they could. Top and bottom, as I understand them, are set by the font (hence "FontMetrics"), so a font maker could make a glyph go higher than whatever they say the top is (or lower than the bottom). Also, with combining diacritical marks in Unicode it can very easily happen. Here is a rather extreme example (taken from here): M̵̳̙͔̟̱͕̓̀̄̉̅ͧ̋͊͌͑́͌ͪ̒̿̀̚a͔̟̝͔ͥ̈́̏ͮͯ̇͆̊̒ͦͦ͘͢͜y̵̴̢͕̝̩̱͈͕̼̣͕̟̌͗̾ͤ̎͌̄ͣͨ͊ͬb̡̯̰̪̜͙̟̝̠͚̜̥̙̤̃ͨ̋̒̒̊ͧͤ͐̓͋̌̾̇̔̈́̀́͡͠e̵ͯͪ̿̿̂̄ͫ̃҉͏͎̣̹̱̜͉̦̞̪̘̠̝̝͍̼̜̖̥̭͟ ̣̞͙͚̝̰̞̹̗̲̣͙͍͍̀̓͊̂̋ͣ̏̑̍̊͌ͩ͐̎̀ͣͣ̚͟ͅh̛͋̏̍̆ͤ͛͐ͨ̌̋ͤ̎̂ͨ̂̓̑̚̕͟͏̻̣͖̖͚͚͓̲̼̪ȁ̔̅̿͐̑͡͏̝͓̮͚̘̦̰͚͎͔͉͚̮̠̕͜ͅṱ̱̼̖̓̂ͭ̏̅͂ͥ͌ͯ͌͠sͪ̓ͪ̄̌̓ͧ͋͐ͬ̅̑҉̨̪̬͎͍̥̬?̡̮̳͙͓͔̹̘̹͓̘̻̦̣͎̫̐ͤ̐͛́͝ ̧̦̼̘͕̪̠̙͖̦̯̦̘͉͈͕͔̘̻̲͑ͨ̊̈́̐ͫ͐̌ͯ̀͘͝Ḩ̷̸̸̹͉̩̜̹̞ͯ̃̃ͧͬͨ̌̀̾̐̈̇ͧ͛̃͐̀ͦ͞A̴̦̗̬̠͙̭͉̟̺͇̭̰͔͕̯̅̃͋ͪ̈́̉̓̌ͯ̈́͆̋̀ͤ̇̂̿̈́̂͡͡Ṱ̲͎͉̣̳̺̱̜̦̬͕̣͉͇͊̌ͥ͐͒̈́̓́ͥ́́̋͂̅ͬ̆͗ͥ̕͢͡S̍ͧ͗̒͗̂̈ͬ͊̚̚͢͏̗̣̳ͅ!̶̨̡͇͚̙͚̭̱̣̲̳̤̞̫̗̣̦̮̖̞͒͆̿̄͑̃̎͡
Plugging that string into Android we get this:
The diacritical marks go above the top
and below the bottom
. It is interesting to note that the total width and height are correctly measured by the text bounds, though.
Anyway, for all practical purposes in your programming, you can just assume that the max and min for glyph letters are top
and bottom
. And usually they will stay within ascent
and descent
. If for whatever reason you need to know for sure if the letters go beyond top
or bottom
you can use TextPaint.getTextBounds
.
Android Layout: meaning of getLineBaseline, getLineDescent, getLineAscent, getLineBottom, getLineTop
I have previously described the meaning of top, ascent, baseline, descent, bottom, and leading in Android's FontMetrics.
Because the Layout
methods getLineBaseline
, getLineDescent
, getLineAscent
, getLineBottom
, and getLineTop
sound so similar to the FontMetrics
names, it is easy to get them confused. However, they report two different types of things:
These methods return their vertical positions on the layout, which is different for every line.
getLineBaseline
getLineBottom
getLineTop
However, the following two methods return the value for the particular line they are on, regardless of where the line is in the layout. So unless there are special spans that affect the size, they will be the same for every line.
getLineAscent
getLineDescent
Demo
I made a simple project to demonstrate that the imformation above. There are six lines of text in an EditText
. Clicking the button logs the info for each line.
Results
Here is the logged result:
line 0 baseline: 67
line 1 baseline: 140
line 2 baseline: 213
line 3 baseline: 286
line 4 baseline: 359
line 5 baseline: 432
line 0 descent: 15
line 1 descent: 15
line 2 descent: 15
line 3 descent: 15
line 4 descent: 15
line 5 descent: 18
line 0 ascent: -67
line 1 ascent: -58
line 2 ascent: -58
line 3 ascent: -58
line 4 ascent: -58
line 5 ascent: -58
line 0 top: 0
line 1 top: 82
line 2 top: 155
line 3 top: 228
line 4 top: 301
line 5 top: 374
line 0 bottom: 82
line 1 bottom: 155
line 2 bottom: 228
line 3 bottom: 301
line 4 bottom: 374
line 5 bottom: 450
FontMetrics top: -67
FontMetrics bottom: 18
FontMetrics ascent: -58
FontMetrics descent: 15
As you can see, top, bottom, and baseline are cumulative based on the line. Ascent and descent mainly stay the same for each line. Ascent is equal to FontMetrics.ascent
for all lines except the first line, where it equals FontMetrics.top
. And descent is equal to FontMetrics.descent
for all lines except the last line, where it equals FontMetrics.bottom
.
So top, bottom, baseline, ascent, and descent for a line should not be considered to be equal to the FontMetrics
values of the same names. On a line ascent is the distance from the baseline to the bottom of the line above it. Descent is the distance from the baseline to the top of the next line.
In the source code, only top
and descent
are saved for every line. The other values are calculated from them:
- bottom = top of next line
- baseline = bottom - descent
- ascent = top - (bottom - descent)
Project code:
public class MainActivity extends AppCompatActivity {
EditText editText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editText = (EditText) findViewById(R.id.editText);
}
public void buttonClick(View view) {
Layout layout = editText.getLayout();
for (int i = 0; i < layout.getLineCount(); i++) {
int baseline = layout.getLineBaseline(i);
Log.i("TAG", "line " + i + " baseline: " + baseline);
}
for (int i = 0; i < layout.getLineCount(); i++) {
int descent = layout.getLineDescent(i);
Log.i("TAG", "line " + i + " descent: " + descent);
}
for (int i = 0; i < layout.getLineCount(); i++) {
int ascent = layout.getLineAscent(i);
Log.i("TAG", "line " + i + " ascent: " + ascent);
}
for (int i = 0; i < layout.getLineCount(); i++) {
int top = layout.getLineTop(i);
Log.i("TAG", "line " + i + " top: " + top);
}
for (int i = 0; i < layout.getLineCount(); i++) {
int bottom = layout.getLineBottom(i);
Log.i("TAG", "line " + i + " bottom: " + bottom);
}
Paint.FontMetricsInt fm = editText.getLayout().getPaint().getFontMetricsInt();
Log.i("TAG", "fm top: " + fm.top);
Log.i("TAG", "fm bottom: " + fm.bottom);
Log.i("TAG", "fm ascent: " + fm.ascent);
Log.i("TAG", "fm descent: " + fm.descent);
}
}
See also
Layout
documentationLayout.java
source codeStaticLayout.java
source code
Align two texts by font's ascent instead of baseline in Jetpack Compose
All information about text layout can be retrieved with onTextLayout
Text
argument. In this case you need a line size, which can be retrieved with getLineBottom
, and an actual font size, which can be found in layoutInput.style.fontSize
.
I agree that it'd be easier if you could use some simple way to do that, so I've starred your feature request, but for now here's how you can calculate it:
onTextLayout = { textLayoutResult ->
val ascent = textLayoutResult.getLineBottom(0) - textLayoutResult.layoutInput.run {
with(density) {
style.fontSize.toPx()
}
}
},
Full example of aligning two texts:
val ascents = remember { mutableStateMapOf<Int, Float>() }
val texts = remember {
listOf(
"Big text" to 80.sp,
"Small text" to 20.sp,
)
}
Row(
Modifier
.drawBehind {
ascents.maxOfOrNull { it.value }?.let {
drawLine(Color.Red, Offset(0f, it), Offset(size.width, it))
}
}
) {
texts.forEachIndexed { i, info ->
Text(
info.first,
fontSize = info.second,
onTextLayout = { textLayoutResult ->
ascents[i] = textLayoutResult.getLineBottom(0) - textLayoutResult.layoutInput.run {
with(density) {
style.fontSize.toPx()
}
}
},
modifier = Modifier
.alpha(if (ascents.count() == texts.count()) 1f else 0f)
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val maxAscent = ascents.maxOfOrNull { it.value } ?: 0f
val ascent = ascents[i] ?: 0f
val yOffset = if (maxAscent == ascent) 0 else (maxAscent - ascent).toInt()
layout(placeable.width, placeable.height - yOffset) {
placeable.place(0, yOffset)
}
}
)
}
}
Result:
How to get text BaseLine from Paint
All the calculations of paint.getFontMetrics()
will happen with respect to baseLine. So if i just subtract (getMeasuredHeight() / 2f) - (fontMetrics.ascent / 2f)
it will draw from center
Java: FontMetrics ascent incorrect?
One possible reason is that this value takes into accounts letters with diacritics.
For example, adding the umlauts ÄÖÜ shows that their trema are much closer to the ascent (although they still don't quite reach it).
Looking for a more general definition of ascent I find the definition in Wikipedia:
[..] the ascent spans the distance between the baseline and the top of the glyph that reaches farthest from the baseline. The ascent and descent may or may not include distance added by accents or diacritical marks.
So it seems that even within typography there is no precise, absolute definition.
FontMetrics not correct when run on android device. Simulator fine
There are two things I would like to mention.
In my experience I have calculated font height in pixels by subtracting FontMetrics.top from FontMetrics.bottom. This is due to the positive and negative values for bottom and top as per Y axis. Please see android documentation for this. So I would change your calculateHeight method as follows:
public static float calculateHeight(FontMetrics fm) {
return fm.bottom - fm.top;
}
Secondly you should remember that your determineTextSize method would return the size in pixels. If you are using this to set the Text Size of a TextView or something then you should specify the units as TypedValue.COMPLEX_UNIT_PX. The default unit for this method is TypedValue.COMPLEX_UNIT_SP
Related Topics
How to Set Layout_Weight Attribute Dynamically from Code
Android: Taking Complete Control of Phone(Kiosk Mode), Is It Possible? How
Android - How to Receive Broadcast Intents Action_Screen_On/Off
Android Canvas: Drawing Too Large Bitmap
Android Inject_Events Permission
Finish All Activities at a Time
How to Start Service Using Alarm Manager in Android
How to Bring an Activity to Foreground (Top of Stack)
Spinner: Get State or Get Notified When Opens
Published App on Play Store Can't Communicate with Google Maps API and Facebook API
Too Many Field References: 70613; Max Is 65536
Navigationview Onnavigationitemselectedlistener Not Being Called
How to Implement Multi-Select in Recyclerview
Startforeground Fail After Upgrade to Android 8.1
Start an Activity with a Parameter
How to Set Image Button Backgroundimage for Different State
How to Configure a Static Ip Address, Netmask, Gateway Programmatically on Android 3.X or 4.X