202

I'm measuring text using Paint.getTextBounds(), since I'm interested in getting both the height and width of the text to be rendered. However, the actual text rendered is always a bit wider than the .width() of the Rect information filled by getTextBounds().

To my surprise, I tested .measureText(), and found that it returns a different (higher) value. I gave it a try, and found it correct.

Why do they report different widths? How can I correctly obtain the height and width? I mean, I can use .measureText(), but then I wouldn't know if I should trust the .height() returned by getTextBounds().

As requested, here is minimal code to reproduce the problem:

final String someText = "Hello. I believe I'm some text!";

Paint p = new Paint();
Rect bounds = new Rect();

for (float f = 10; f < 40; f += 1f) {
    p.setTextSize(f);

    p.getTextBounds(someText, 0, someText.length(), bounds);

    Log.d("Test", String.format(
        "Size %f, measureText %f, getTextBounds %d",
        f,
        p.measureText(someText),
        bounds.width())
    );
}

The output shows that the difference not only gets greater than 1 (and is no last-minute rounding error), but also seems to increase with size (I was about to draw more conclusions, but it may be entirely font-dependent):

D/Test    (  607): Size 10.000000, measureText 135.000000, getTextBounds 134
D/Test    (  607): Size 11.000000, measureText 149.000000, getTextBounds 148
D/Test    (  607): Size 12.000000, measureText 156.000000, getTextBounds 155
D/Test    (  607): Size 13.000000, measureText 171.000000, getTextBounds 169
D/Test    (  607): Size 14.000000, measureText 195.000000, getTextBounds 193
D/Test    (  607): Size 15.000000, measureText 201.000000, getTextBounds 199
D/Test    (  607): Size 16.000000, measureText 211.000000, getTextBounds 210
D/Test    (  607): Size 17.000000, measureText 225.000000, getTextBounds 223
D/Test    (  607): Size 18.000000, measureText 245.000000, getTextBounds 243
D/Test    (  607): Size 19.000000, measureText 251.000000, getTextBounds 249
D/Test    (  607): Size 20.000000, measureText 269.000000, getTextBounds 267
D/Test    (  607): Size 21.000000, measureText 275.000000, getTextBounds 272
D/Test    (  607): Size 22.000000, measureText 297.000000, getTextBounds 294
D/Test    (  607): Size 23.000000, measureText 305.000000, getTextBounds 302
D/Test    (  607): Size 24.000000, measureText 319.000000, getTextBounds 316
D/Test    (  607): Size 25.000000, measureText 330.000000, getTextBounds 326
D/Test    (  607): Size 26.000000, measureText 349.000000, getTextBounds 346
D/Test    (  607): Size 27.000000, measureText 357.000000, getTextBounds 354
D/Test    (  607): Size 28.000000, measureText 369.000000, getTextBounds 365
D/Test    (  607): Size 29.000000, measureText 396.000000, getTextBounds 392
D/Test    (  607): Size 30.000000, measureText 401.000000, getTextBounds 397
D/Test    (  607): Size 31.000000, measureText 418.000000, getTextBounds 414
D/Test    (  607): Size 32.000000, measureText 423.000000, getTextBounds 418
D/Test    (  607): Size 33.000000, measureText 446.000000, getTextBounds 441
D/Test    (  607): Size 34.000000, measureText 455.000000, getTextBounds 450
D/Test    (  607): Size 35.000000, measureText 468.000000, getTextBounds 463
D/Test    (  607): Size 36.000000, measureText 474.000000, getTextBounds 469
D/Test    (  607): Size 37.000000, measureText 500.000000, getTextBounds 495
D/Test    (  607): Size 38.000000, measureText 506.000000, getTextBounds 501
D/Test    (  607): Size 39.000000, measureText 521.000000, getTextBounds 515
salezica
  • 74,081
  • 25
  • 105
  • 166
  • You can see [my way][1]. That can get the position and correct bound. [1]: http://stackoverflow.com/a/19979937/1621354 – Alex Chi Nov 14 '13 at 14:18
  • [Related explanation about the methods of measuring text](https://stackoverflow.com/a/42091739/3681880) for other visitors to this question. – Suragch Aug 27 '17 at 07:54

8 Answers8

396

You can do what I did to inspect such problem:

Study Android source code, Paint.java source, see both measureText and getTextBounds methods. You'd learn that measureText calls native_measureText, and getTextBounds calls nativeGetStringBounds, which are native methods implemented in C++.

So you'd continue to study Paint.cpp, which implements both.

native_measureText -> SkPaintGlue::measureText_CII

nativeGetStringBounds -> SkPaintGlue::getStringBounds

Now your study checks where these methods differ. After some param checks, both call function SkPaint::measureText in Skia Lib (part of Android), but they both call different overloaded form.

Digging further into Skia, I see that both calls result into same computation in same function, only return result differently.

To answer your question: Both your calls do same computation. Possible difference of result lies in fact that getTextBounds returns bounds as integer, while measureText returns float value.

So what you get is rounding error during conversion of float to int, and this happens in Paint.cpp in SkPaintGlue::doTextBounds in call to function SkRect::roundOut.

The difference between computed width of those two calls may be maximally 1.

EDIT 4 Oct 2011

What may be better than visualization. I took the effort, for own exploring, and for deserving bounty :)

enter image description here

This is font size 60, in red is bounds rectangle, in purple is result of measureText.

It's seen that bounds left part starts some pixels from left, and value of measureText is incremented by this value on both left and right. This is something called Glyph's AdvanceX value. (I've discovered this in Skia sources in SkPaint.cpp)

So the outcome of the test is that measureText adds some advance value to the text on both sides, while getTextBounds computes minimal bounds where given text will fit.

Hope this result is useful to you.

Testing code:

  protected void onDraw(Canvas canvas){
     final String s = "Hello. I'm some text!";

     Paint p = new Paint();
     Rect bounds = new Rect();
     p.setTextSize(60);

     p.getTextBounds(s, 0, s.length(), bounds);
     float mt = p.measureText(s);
     int bw = bounds.width();

     Log.i("LCG", String.format(
          "measureText %f, getTextBounds %d (%s)",
          mt,
          bw, bounds.toShortString())
      );
     bounds.offset(0, -bounds.top);
     p.setStyle(Style.STROKE);
     canvas.drawColor(0xff000080);
     p.setColor(0xffff0000);
     canvas.drawRect(bounds, p);
     p.setColor(0xff00ff00);
     canvas.drawText(s, 0, bounds.bottom, p);
  }
Marcin Orlowski
  • 72,056
  • 11
  • 123
  • 141
Pointer Null
  • 39,597
  • 13
  • 90
  • 111
  • I think the results @mice found are missleading. The observations might be correct for the font size of 60 but they turn much more different when the text is smaller. Eg. 10px. In that case the text is actually drawn BEYOND the bounds. – Moritz Jun 01 '12 at 15:40
  • 19
    @mice I just found this question again (I'm the OP). I had forgotten how incredible your answer was. Thanks from the future! Best 100rp I have invested! – salezica Nov 23 '12 at 19:20
  • Guys, there is a simple explanation of this incomprehensible behavior. Found it here: http://stackoverflow.com/a/14766372/1048087 See details in my answer. – Prizoff Mar 13 '13 at 23:35
  • Does anyone know what the unit of measurement is for bounds? Is it in pixels? I got them checked in photoshop with a sample app I created; the measured width which measureText gives me is 81 and when I check them in photoshop the width is 75px. Also, is the units of measurement same for FontMetrics as well? – Vikram Gupta Apr 06 '16 at 02:03
  • Graphics functions generally work with pixels. Only resources hold values in various units (px/dp/sp). – Pointer Null Apr 06 '16 at 16:12
22

My experience with this is that getTextBounds will return that absolute minimal bounding rect that encapsulates the text, not necessarily the measured width used when rendering. I also want to say that measureText assumes one line.

In order to get accurate measuring results, you should use the StaticLayout to render the text and pull out the measurements.

For example:

String text = "text";
TextPaint textPaint = textView.getPaint();
int boundedWidth = 1000;

StaticLayout layout = new StaticLayout(text, textPaint, boundedWidth , Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
int height = layout.getHeight();
Chase
  • 11,161
  • 8
  • 42
  • 39
  • Didn't know about StaticLayout! getWidth() won't work, it just returns what I pass in the costructor (which is `width`, not `maxWidth`), but getLineWith() will. I also found the static method getDesiredWidth(), though there's no height equivalent. Please correct the answer! Also, I would really like to know why so many different methods yield different results. – salezica Oct 04 '11 at 13:50
  • My bad regarding the width. If you want the width of some text that will be rendered on a single line, the `measureText` should work fine. Otherwise, if you know the width constaints (such as in `onMeasure` or `onLayout`, you can use the solution I posted above. Are you trying to auto resize text perchance? Also, the reason text bounds is always slightly smaller is because that it excludes any character padding, just the smallest bounded rect needed for drawing. – Chase Oct 04 '11 at 17:48
  • I am indeed trying to auto-resize a TextView. I need the height as well, that's where my problems started! measureWidth() works fine otherwise. After that, I got curious as to why different approaches gave different values – salezica Oct 04 '11 at 19:00
  • 1
    One thing to also note is that a textview that wraps and is a multiline will measure at match_parent for width and not just the required width, which makes the above width param to StaticLayout valid. If you need a text resizer, check out my post at http://stackoverflow.com/questions/5033012/auto-scale-textview-text-to-fit-within-bounds/5535672#5535672 – Chase Oct 05 '11 at 00:35
  • I needed to have a text expansion animation and this helped a bunch! Thank you! – box May 20 '16 at 08:23
  • This is the only method that can measure the height of multiline texts properly. – fire in the hole Sep 21 '17 at 06:54
19

The mice answer is great... And here is the description of the real problem:

The short simple answer is that Paint.getTextBounds(String text, int start, int end, Rect bounds) returns Rect which doesn't starts at (0,0). That is, to get actual width of text that will be set by calling Canvas.drawText(String text, float x, float y, Paint paint) with the same Paint object from getTextBounds() you should add the left position of Rect. Something like that:

public int getTextWidth(String text, Paint paint) {
    Rect bounds = new Rect();
    paint.getTextBounds(text, 0, end, bounds);
    int width = bounds.left + bounds.width();
    return width;
}

Notice this bounds.left - this the key of the problem.

In this way you will receive the same width of text, that you would receive using Canvas.drawText().

And the same function should be for getting height of the text:

public int getTextHeight(String text, Paint paint) {
    Rect bounds = new Rect();
    paint.getTextBounds(text, 0, end, bounds);
    int height = bounds.bottom + bounds.height();
    return height;
}

P.s.: I didn't test this exact code, but tested the conception.


Much more detailed explanation is given in this answer.

Community
  • 1
  • 1
Prizoff
  • 4,486
  • 4
  • 41
  • 69
  • 2
    Rect defines (b.width) to be (b.right-b.left) and (b.height) to be (b.bottom-b.top). Therefore you can replace (b.left+b.width) with (b.right) and (b.bottom+b.height) with (2*b.bottom-b.top), but I think you want to have (b.bottom-b.top) instead, which is the same as (b.height). I think you're correct about the width (based on checking the C++ source), getTextWidth() returns the same value as (b.right), there might be some rounding errors. (b is a reference to bounds) – Nulano Aug 04 '15 at 10:19
12

Sorry for answering again on that question... I needed to embed the image.

I think the results @mice found are missleading. The observations might be correct for the font size of 60 but they turn much more different when the text is smaller. Eg. 10px. In that case the text is actually drawn BEYOND the bounds.

enter image description here

Sourcecode of the screenshot:

  @Override
  protected void onDraw( Canvas canvas ) {
    for( int i = 0; i < 20; i++ ) {
      int startSize = 10;
      int curSize = i + startSize;
      paint.setTextSize( curSize );
      String text = i + startSize + " - " + TEXT_SNIPPET;
      Rect bounds = new Rect();
      paint.getTextBounds( text, 0, text.length(), bounds );
      float top = STEP_DISTANCE * i + curSize;
      bounds.top += top;
      bounds.bottom += top;
      canvas.drawRect( bounds, bgPaint );
      canvas.drawText( text, 0, STEP_DISTANCE * i + curSize, paint );
    }
  }
Moritz
  • 10,124
  • 7
  • 51
  • 61
  • Ah, the mistery drags on. I'm still interested in this, even if out of curiosity. Let me know if you find out anything else, please! – salezica Jun 03 '12 at 00:45
  • The problem can be remedied a little when you multiply the fontsize you want to measure by a factor of lets say 20 and than divide the result by that. THAN you might also want to add 1-2% more width of the measured size just to be on the safe side. In the end you will have text that is measured as to long but at least there is no text overlapping. – Moritz Jun 04 '12 at 16:24
  • 7
    To draw the text exactly inside the bounding rect you mustn't draw Text with X=0.0f, because bounding rect is returning like rect, not like width and height. To draw it correctly you should offset the X coordinate on "-bounds.left" value, then it will be shown correctly. – Roman Jul 11 '14 at 13:56
12

The different between getTextBounds and measureText is described with the image below.

In short,

  1. getTextBounds is to get the RECT of the exact text. The measureText is the length of the text, including the extra gap on the left and right.

  2. If there are spaces between the text, it is measured in measureText but not including in the length of the TextBounds, although the coordinate get shifted.

  3. The text could be tilted (Skew) left. In this case, the bounding box left side would exceed outside the measurement of the measureText, and the overall length of the text bound would be bigger than measureText

  4. The text could be tilted (Skew) right. In this case, the bounding box right side would exceed outside the measurement of the measureText, and the overall length of the text bound would be bigger than measureText

enter image description here

Elye
  • 53,639
  • 54
  • 212
  • 474
10

DISCLAIMER: This solution is not 100% accurate in terms of determining the minimal width.

I was also figuring out how to measure text on a canvas. After reading the great post from mice i had some problems on how to measure multiline text. There is no obvious way from these contributions but after some research i cam across the StaticLayout class. It allows you to measure multiline text (text with "\n") and configure much more properties of your text via the associated Paint.

Here is a snippet showing how to measure multiline text:

private StaticLayout measure( TextPaint textPaint, String text, Integer wrapWidth ) {
    int boundedWidth = Integer.MAX_VALUE;
    if (wrapWidth != null && wrapWidth > 0 ) {
       boundedWidth = wrapWidth;
    }
    StaticLayout layout = new StaticLayout( text, textPaint, boundedWidth, Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false );
    return layout;
}

The wrapwitdh is able to determin if you want to limit your multiline text to a certain width.

Since the StaticLayout.getWidth() only returns this boundedWidth you have to take another step to get the maximum width required by your multiline text. You are able to determine each lines width and the max width is the highest line width of course:

private float getMaxLineWidth( StaticLayout layout ) {
    float maxLine = 0.0f;
    int lineCount = layout.getLineCount();
    for( int i = 0; i < lineCount; i++ ) {
        if( layout.getLineWidth( i ) > maxLine ) {
            maxLine = layout.getLineWidth( i );
        }
    }
    return maxLine;
}
grebulon
  • 7,697
  • 5
  • 42
  • 66
Moritz
  • 10,124
  • 7
  • 51
  • 61
2

There is another way to measure the text bounds precisely, first you should get the path for the current Paint and text. In your case it should be like this:

p.getTextPath(someText, 0, someText.length(), 0.0f, 0.0f, mPath);

After that you can call:

mPath.computeBounds(mBoundsPath, true);

In my code it always returns correct and expected values. But, not sure if it works faster than your approach.

Roman
  • 350
  • 2
  • 8
0

This is how I calculated the real dimensions for the first letter (you can change the method header to suit your needs, i.e. instead of char[] use String):

private void calculateTextSize(char[] text, PointF outSize) {
    // use measureText to calculate width
    float width = mPaint.measureText(text, 0, 1);

    // use height from getTextBounds()
    Rect textBounds = new Rect();
    mPaint.getTextBounds(text, 0, 1, textBounds);
    float height = textBounds.height();
    outSize.x = width;
    outSize.y = height;
}

Note that I'm using TextPaint instead of the original Paint class.

milosmns
  • 3,595
  • 4
  • 36
  • 48