Truly aligning text in Android TextView
I'm trying to display a TextView in Android so that the text in the view is top-aligned:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create container layout
FrameLayout layout = new FrameLayout(this);
// Create text label
TextView label = new TextView(this);
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, 25); // 25 pixels tall
label.setGravity(Gravity.TOP + Gravity.CENTER); // Align text top-center
label.setPadding(0, 0, 0, 0); // No padding
Rect bounds = new Rect();
label.getPaint().getTextBounds("gdyl!", 0, 5, bounds); // Measure height
label.setText("good day, world! "+bounds.top+" to "+bounds.bottom);
label.setTextColor (0xFF000000); // Black text
label.setBackgroundColor(0xFF00FFFF); // Blue background
// Position text label
FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(300, 25, Gravity.LEFT + Gravity.TOP);
// also 25 pixels tall
layoutParams.setMargins(50, 50, 0, 0);
label.setLayoutParams(layoutParams);
// Compose screen
layout.addView(label);
setContentView(layout);
}
This code outputs the following image:
Points to note:
- The blue box is 25px high, just like the request
- Text borders are also reported as 25px high as requested (6 - (-19) = 25)
- The text does not start at the top of the label, but has a caption above it, ignoring setPadding ()
- This causes the text to be clipped at the bottom, although the box is technically high
How do I tell the TextView to start text at the very top of the window?
I have two restrictions on the possible answers:
- I need the text to be top-aligned, so if there is some trick to bottom-align or center it vertically, I cannot use it as I have scripts where the TextView is taller than it should to be.
- I'm a bit like compatibility, so if possible I would like to stick with calls that were available in early Android APIs (preferably 1, but definitely not higher than 7).
source to share
TextViews use the abstract class android.text.Layout to draw text on the canvas:
canvas.drawText(buf, start, end, x, lbaseline, paint);
The vertical offset is lbaseline
calculated as the bottom of the line minus the font descent:
int lbottom = getLineTop(i+1);
int lbaseline = lbottom - getLineDescent(i);
The two callable functions getLineTop
and getLineDescent
are abstract, but a simple implementation can be found in BoringLayout (go figure ... :), which simply returns its values for mBottom
and mDesc
. They are calculated in the init method as follows:
if (includepad) {
spacing = metrics.bottom - metrics.top;
} else {
spacing = metrics.descent - metrics.ascent;
}
if (spacingmult != 1 || spacingadd != 0) {
spacing = (int)(spacing * spacingmult + spacingadd + 0.5f);
}
mBottom = spacing;
if (includepad) {
mDesc = spacing + metrics.top;
} else {
mDesc = spacing + metrics.ascent;
}
Here includepad
is a boolean value that indicates whether the text should include additional padding to display glyphs that pass in the specified ascent. It can be set (as @ggc pointed out) using the TextView's setIncludeFontPadding method .
If the includesepad parameter is set to true (the default), the text is positioned with the base value specified by the top
-field of the metric font. Otherwise, the baseline of the text will be taken from descent
-field .
So, technically, this should mean that all we have to do is disable IncludeFontPadding, but unfortunately this gives the following output:
The reason for this is that the font reports -23.2 as a bubble, and the bounding box reports an upper value of -19. I don't know if this is a "bug" in the font or if it should be. Unfortunately, FontMetrics does not provide any value that matches the 19 given in the bounding box, even if you try to somehow include the 240dpi screen resolution message versus 72dpi font point detection, so there is no "official" way to fix this ...
But, of course, the available information can be used to crack the solution. This can be done in two ways:
-
with IncludeFontPadding left alone, i.e. set to true:
double top = label.getPaint().getFontMetrics().top; label.setPadding(0, (int) (top - bounds.top - 0.5), 0, 0);
i.e. the vertical padding is set to compensate for the difference in the y-value specified in the text borders and the upper-font value. Result:
-
with IncludeFontPadding set to false:
double ascent = label.getPaint().getFontMetrics().ascent; label.setPadding(0, (int) (ascent - bounds.top - 0.5), 0, 0); label.setIncludeFontPadding(false);
i.e. vertical padding is set to compensate for differences in the y-value specified in the text boundaries and the ascending font-value. Result:
Note that there is nothing magical about IncludeFontPadding being false. Both versionsmust work. The reason they give different results is slightly different rounding errors when floating point font values are converted to integers. It just so happens that it looks better in this particular case if IncludeFontPadding is set to false, but this may be different for different fonts or font sizes. It is probably quite easy to tweak the top padding calculation to get the same exact rounding errors as the calculation used by BoringLayout. I haven't done that yet, as I'm more likely to use an error-free font instead, but I can add it later if I find it for a while. Then it should really be inappropriate whether the IncludeFontPadding is set to false or true.
source to share
If your TextView is inside a different layout, make sure there is enough space between them. You can add indentation at the bottom of the parent view and see if you have the full text. It worked for me!
Example: you have a textView inside a FrameLayout, but the FrameLayout is too small and shrinks your textView. Add padding to your FrameLayout to see if it works.
Edit : change this line
FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(300, 25, Gravity.LEFT + Gravity.TOP);
for this line
FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(300, 50, Gravity.LEFT + Gravity.TOP);
This will make the window larger and, in the same way, provide enough space for your text.
OR add this line
label.setIncludeFontPadding(false);
This will remove the surrounding font and allow the text to be seen. But the only thing that doesn't work in your case is that it won't show completely the letters like "g" that go under the line ... You may need to resize the window or text a bit (like 2-3), if you really want it to work.
source to share