4

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).
Markus A.
  • 12,349
  • 8
  • 52
  • 116
  • I suggest you to look at this; http://stackoverflow.com/questions/7549182/android-paint-measuretext-vs-gettextbounds I believe your textSize is 25 px but your textView's height is more that 25, (I try 32 px and it center all layout perfectly.) – Sinan Kozak Nov 01 '14 at 09:07

2 Answers2

11

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.

Markus A.
  • 12,349
  • 8
  • 52
  • 116
  • This is interesting. Can you please point out what should be done in case I want to extend from TextView and add this feature inside it? – android developer Nov 04 '14 at 22:09
  • 1
    @androiddeveloper I guess you would need to override the setTextSize method and have it do a quick getTextBounds on some representative tall letters (TIHdi, and others) in the new size to get the maximum top bounds and then just use that to set the padding accordingly. And make sure you call this setTextSize method at least once (maybe in a new constructor). – Markus A. Nov 04 '14 at 22:19
  • 1
    @androiddeveloper By the way: If you do this, there will be letters (probably Chinese characters and such) that will no longer fit your TextView and will be cut off on the top (and maybe bottom)! So make sure you test your result with all the letters that you plan on using in your app, which may be a lot more that you'd initially think if you provide edit-fields for users. Some people, I'm sure, use Umlauts or similar in their favorite character names, for example. – Markus A. Nov 04 '14 at 22:26
  • Oh, so it's all because in some languages, the characters need more space... I see. But in some cases, I know which characters are allowed, so is there a way that I will give it the whole characters set that the textView will ever be showing, and let it know what size it will need? – android developer Nov 05 '14 at 06:24
  • 1
    @androiddeveloper There are two issues at play here: On the one hand, the font-metric's ascent and descent *should* describe the extent to which a standard character goes above and below the base-line. For some reason this does not seem to match the bounding boxes that I'm getting for normal characters. This seems like a "bug" in the font (or the Android text libraries) to me, since I would have expected |ascent| + |descent| to equal the font size I specified, but it doesn't. And on the other hand, the font specifies a top and bottom, which allows for even more room for some characters. – Markus A. Nov 05 '14 at 06:40
  • So it's more complicated... So what you are suggesting is just leave it alone? it's just that it looks weird sometimes... – android developer Nov 05 '14 at 07:59
  • 1
    @androiddeveloper I, personally, will try to find a font that doesn't have this issue and if I absolutely must use a font that does have this problem, I will make sure to check all characters that I would expect to use to make sure they don't clip. But, honestly, the easiest solution might be to just position your TextViews 20 pixels higher, make them 40 pixels taller, and add 20 to the top-padding. That way you can still reliably position the text and you won't have any clipping issues. Only thing you won't be able to do is to add a background color to the label... – Markus A. Nov 05 '14 at 19:08
  • 1
    @androiddeveloper And it might increase the area of the label that will capture touch-events... BTW: Another issue: If you use shadow-layers on the text for shadows or blurred glows, the shadow also needs to fit inside the TextView's dimensions to avoid clipping! – Markus A. Nov 05 '14 at 19:09
  • Say, since you seem to know a lot about TextView and fonts, can you please check out a library I've published? It's supposed to auto-fit the text using the restrictions the user has set on the TextView, but it has a few rare issues that I've failed to fix: https://github.com/AndroidDeveloperLB/AutoFitTextView – android developer Nov 10 '14 at 20:23
  • @MarkusA. Can you help me with this http://stackoverflow.com/questions/39120828/how-to-draw-a-line-to-perfectly-align-top-of-a-text .Great answer by the way. – Sunil Sunny Aug 24 '16 at 10:32
  • For those who are looking for how to get bounds, look at the main post. – Kai Wang Mar 22 '18 at 19:19
1

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.

ggc
  • 15
  • 7
  • ? Not sure I'm following your answer... The label is not occluded by any other element as you can see its full height of 25 pixels that I set... It's just that it positions the text inside of it in a way that it doesn't fit inside the label. (see example code) – Markus A. Oct 31 '14 at 21:27
  • But have you tryed at least my answer? I told you that it worked for me, just giving it a try wont hurt. – ggc Nov 03 '14 at 13:56
  • Of course I can just make the label bigger, but that only solves this specific case, and how much bigger I need to make it will depend on the exact font, size, and such. So while your answer will definitely fix the example I posted, I was hoping for a way to actually fit the text into only the size that it should need, by getting rid of the padding above it. Your solution will make the blue box bigger, for example, which is not something that I would want in the design. – Markus A. Nov 03 '14 at 17:31
  • Ok i finally got what you were asking for. You have to add this: 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... Well, see if you can work something around it! – ggc Nov 03 '14 at 18:26
  • This seems to answer the question, so please consider marking this has answered. – ggc Nov 04 '14 at 19:17
  • Unfortunately that doesn't work reliably either. I had some time to figure out a solution... You can see it below. But thanks for your help. +1 – Markus A. Nov 04 '14 at 21:47