Android Paint: .Measuretext() VS .Gettextbounds()

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

Sample Image

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

Android : Paint.getTextBounds returns width much greater than the width of the screen

The documentation states that int end is "1 past the last char in the string to measure." http://developer.android.com/reference/android/graphics/Paint.html#getTextBounds(java.lang.String, int, int, android.graphics.Rect)

You should have:

textView.getPaint().getTextBounds(text, 0, text.length(), rect);

This also applies to Canvas.drawText().

The TextView displaying text would be "wrapping" the text. The method getTextBounds is not aware of the TextView at all.

In fact, if you were to build your own custom View for displaying text, getTextBounds would be very useful for achieving your own text wrapping. The following code does a terrible job of presentation, but iterates over a String, drawing sections of it to a Canvas, effectively wrapping the text.

    private void drawText(Canvas canvas) {

String text = "Nunc eleifend erat eu nulla varius iaculis. Quisque feugiat justo sit amet" +
" neque interdum tempor. Curabitur faucibus facilisis tristique. Donec posuere" +
" viverra magna, eu accumsan sapien congue id. Cras fringilla justo ut lacus" +
" molestie, et venenatis orci egestas. Vivamus pretium, nisl quis cursus cursus," +
" dolor eros porta neque, sit amet viverra ante orci non elit. Donec odio neque," +
" volutpat sit amet dui sit amet, fringilla tempus sapien. Donec sed mollis justo." +
" In dignissim tincidunt neque, eu luctus justo egestas nec. Donec commodo ut arcu" +
" vel placerat. Donec ullamcorper justo eget justo commodo hendrerit. Quisque" +
" commodo imperdiet posuere. Aenean vehicula dui.";

Paint paint = new Paint();
paint.setTextSize(30);
paint.setAntiAlias(true);

int padding = 20;

int availWidth = getWidth() - 2 * padding;
int availHeight = getHeight() - 2 * padding;

Rect bounds = new Rect();
int currentTextBottom = 0;
int firstCharInLine = 0;
int lastCharInLine = 0;

outerLoop: while (currentTextBottom < availHeight) {

while (Character.isWhitespace(text.charAt(firstCharInLine))) {
lastCharInLine = ++firstCharInLine;
}

for (int i = firstCharInLine + 1; i < text.length(); i++) {
paint.getTextBounds(text, firstCharInLine, i, bounds);

if (bounds.width() <= availWidth) {
if (Character.isWhitespace(text.charAt(i))) {
lastCharInLine = i;
}
} else {
currentTextBottom += bounds.height();
if (firstCharInLine == lastCharInLine) {
lastCharInLine = i;
}
canvas.drawText(text, firstCharInLine, lastCharInLine, padding, currentTextBottom + padding, paint);
firstCharInLine = lastCharInLine++;
break;
}
if (i == text.length() - 1) {
currentTextBottom += bounds.height();
lastCharInLine = text.length();
canvas.drawText(text, firstCharInLine, lastCharInLine, padding, currentTextBottom + padding, paint);
break outerLoop;
}
}
}
}
}

EDIT - as a side note, I've just now noticed a difference between bounds.width() and Paint.measureText()! Totally unrelated to the above code, I was having issues with Paint.Align.Center glitching and not always working as expected. So I removed any text alignment and tried:

canvas.drawText(
text,
viewWidth / 2f - bounds.width() / 2f,
viewHeight / 2f + bounds.height() / 2f,
paint);

But this doesn't align horizontally properly. When I did this instead:

float textWidth = paint.measureText(text);
canvas.drawText(
text,
viewWidth / 2f - textWidth / 2,
viewHeight / 2f + bounds.height() / 2f,
paint);

It aligned to my satisfaction :)

Android: measureText() Return Pixels Based on Scaled Pixels

You need to get the densityMultiplier like so:

final float densityMultiplier = getContext().getResources().getDisplayMetrics().density;
final float scaledPx = 20 * densityMultiplier;
paint.setTextSize(scaledPx);
final float size = paint.measureText("sample text");

Paint.measureText: Is the sum of the parts equal to the whole?

The whole is not always equal to the sum of the parts.

This appears to be a result of kerning and not related to any AdvanceX double padding that you mentioned.

Test code

Here is the test code:

String myString = "This is some text.";

Paint myPaint = new Paint();
myPaint.setAntiAlias(true);
myPaint.setTextSize(100);

float theWhole = myPaint.measureText(myString);

float sumOfTheParts = 0;
for (int i = 0; i < myString.length(); i++) {
sumOfTheParts += myPaint.measureText(myString, i, i + 1);
}

Results

In my first test, the measures are the same:

String myString = "This is some text.";

theWhole // 787.0
sumOfTheParts // 787.0

However, when there are letters that need kerning, the measures are different. This is the test again with a different string:

String myString = "AV Wa";

theWhole // 291.0
sumOfTheParts // 297.0

Implications

You cannot assume that measuring all the characters of a string and then summing the total will give you the actual length of the string. Thus, when doing text wrapping you will need to measure entire string ranges.

getTextBounds returning wrong values

The method takes the following parameters:

getTextBounds(char[] text, int index, int count, Rect bounds)

And you request the width of only one character (the third parameter), not the whole string:

paint.getTextBounds("ABCDEFGHI", 0, 1, bounds);

bounds.right is 8, which is the width of the letter A.

The correct call in your case would be:

String str = "ABCDEFGHI";
paint.getTextBounds(str, 0, str.length(), bounds);

android view width is not bigger when a11y is changed to big font

From the image I see that myAccountView expands up to the rounded edges. It's width spans to the rounded edges. But the text starts and ends after the end of left half circle and before the beginning of right half circle. So the padding of myAccountView should be considered. In your edit you considered the padding of myAccountView's parent which has no relevance.

While measuring the chipWidth the padding should be considered.

int chipWidth = myAccountView.getMeasuredWidth() - myAccountView.getPaddingLeft() - myAccountView.getPaddingRight();

How to quickly get width and height of TextView using Paint.getTextBounds()?

Found a simple and not slow method of determining text width/height drawn with specific Paint that doesn't use StaticLayout.

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

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

The short simple description of trick: 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.

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


Much more detailed explanation is given in this answer.



Related Topics



Leave a reply



Submit