Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Paint: .measureText() vs .getTextBounds()

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);
  }

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();

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.


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 );
    }
  }

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;
}