I am trying to display a TextView in Android such 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:
The things to note:
How do I tell the TextView to start the text at the very top of the box?
I have two restrictions to possible answers:
TextViews use the abstract class android.text.Layout to draw the text on the canvas:
canvas.drawText(buf, start, end, x, lbaseline, paint);
The vertical offset lbaseline
is calculated as the bottom of the line minus the font's descent:
int lbottom = getLineTop(i+1);
int lbaseline = lbottom - getLineDescent(i);
The two called 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
. These are calculated in its 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 that specifies whether the text should include additional padding to allow for glyphs that extend past the specified ascent. It can be set (as @ggc pointed out) by the TextView's setIncludeFontPadding method.
If includepad is set to true (the default value), the text is positioned with its baseline given by the top
-field of the font's metrics. Otherwise the text's baseline is taken from the descent
-field.
So, technically, this should mean that all we need to do is to turn off IncludeFontPadding, but unfortunately this yields the following result:
The reason for this is that the font reports -23.2 as its ascent, while the bounding box reports a top-value of -19. I don't know if this is a "bug" in the font or if it's supposed to be like this. Unfortunately the FontMetrics do not provide any value that matches the 19 reported by the bounding box, even if you try to somehow incorporate the reported screen resolution of 240dpi vs. the definition of font points at 72dpi, so there is no "official" way to fix this.
But, of course, the available information can be used to hack a solution. There are two ways to do it:
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 reported from the text bounds and the font-metric's top-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. the vertical padding is set to compensate for the difference in the y-value reported from the text bounds and the font-metric's ascent-value. Result:
Note that there is nothing magical about setting IncludeFontPadding to false. Both version should work. The reason they yield different results are slightly different rounding errors when the font-metric's floating-point values are converted to integers. It just so happens that in this particular case it looks better with IncludeFontPadding set to false, but for different fonts or font sizes this may be different. It is probably fairly easy to adjust the calculation of the top-padding to yield the same exact rounding errors as the calculation used by BoringLayout. I haven't done this yet since I'll rather use a "bug-free" font instead, but I might add it later if I find some time. Then, it should be truly irrelevant whether IncludeFontPadding is set to false or true.
If your TextView is inside an other layout, make sure to check if there is enough space between them. You can add a padding at the bottom of your parent view and see if you get your full text. It worked for me!
Example: you have a textView inside a FrameLayout but the FrameLayout is too small and is cutting your textView. Add a padding to your FrameLayout to see if it work.
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 box bigger and, by the same way, let enough space for your text to be shown.
OR add this line
label.setIncludeFontPadding(false);
This will remove surrounding font padding and let the text be seen. But the only thing that dont work in your case is that it wont show entirely letters like 'g' that goes under the line... Maybe that you will have to change the size of the box or the text just a little (like by 2-3) if you really want it to work.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With