Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Truly top-aligning text in Android TextView

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:

enter image description here

The things to note:

  • The blue box is 25 pixels tall, just like requested
  • The text bounds are also reported as 25 pixels tall as requested (6 - (-19) = 25)
  • The text does not start at the top of the label, but has some padding above it, ignoring setPadding()
  • This leads to the text being clipped at the bottom, even though the box technically is tall enough

How do I tell the TextView to start the text at the very top of the box?

I have two restrictions to possible answers:

  • I do need to keep the text top-aligned, though, so if there is some trick with bottom-aligning or centering it vertically instead, I can't use it, since I have scenarios where the TextView is taller than it needs to be.
  • I'm a bit of a compatibility-freak, so if possible I'd like to stick to calls that were available in the early Android APIs (preferably 1, but definitely no higher than 7).
like image 828
Markus A. Avatar asked Dec 15 '22 18:12

Markus A.


2 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:

enter image description here

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:

    enter image description here

  • 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:

    enter image description here

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.

like image 191
Markus A. Avatar answered Dec 31 '22 04:12

Markus A.


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.

like image 27
ggc Avatar answered Dec 31 '22 03:12

ggc