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

我使用Paint.getTextBounds()测量文本,因为我对要渲染的文本的高度和宽度都感兴趣。然而,实际呈现的文本总是比由getTextBounds()填充的Rect信息的.width()宽一些。

令我惊讶的是,我测试了.measureText(),发现它返回了一个不同的(更高的)值。我试了一下,发现它是正确的。

为什么他们报告的宽度不同?如何正确获取高度和宽度?我的意思是,我可以使用.measureText(),但我不知道是否应该相信getTextBounds()返回的.height()

根据要求,这里是重现问题的最小代码:

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

输出显示,差异不仅大于1(并且没有最后一刻的舍入错误),而且似乎随着大小的增加而增加(我即将得出更多的结论,但它可能完全依赖于字体):

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
111371 次浏览

你可以像我一样检查这样的问题:

研究Android源代码,Paint.java源代码,参见measureText和getTextBounds方法。 您将了解到measureText调用native_measureText, getTextBounds调用nativeGetStringBounds,这是c++实现的本机方法

因此,您将继续学习Paint.cpp,它实现了这两者。

native_measureText -> SkPaintGlue::measureText_CII

nativeGetStringBounds -> SkPaintGlue::getStringBounds

现在你的研究检查这些方法的不同之处。 在一些参数检查后,两者都调用Skia Lib (Android的一部分)中的SkPaint::measureText函数,但它们都调用不同的重载形式

深入挖掘Skia,我发现两个调用的结果都是同一个函数中的相同计算,只是返回的结果不同。

回答你的问题: 两个调用都执行相同的计算。结果的可能差异在于getTextBounds返回的是整数形式的边界,而measureText返回的是浮点值

所以你得到的是在float转换为int时的舍入错误,这发生在SkPaintGlue::doTextBounds中的Paint.cpp中调用SkRect::roundOut函数时。

这两个调用的计算宽度之间的差值最大可以是1。

2011年10月4日

还有什么比想象更好的呢?我努力了,为了自己的探索,也为了应得的奖赏:)

enter image description here

这是字体大小60,红色是界限矩形,紫色是measureText的结果。

可以看到,bounds left部分从左边开始一些像素,measureText的值在左右两边都加了这个值。这就是所谓的Glyph的AdvanceX值。(我在Skia源SkPaint.cpp中发现了这一点)

因此,测试的结果是measureText向两侧的文本添加了一些提前值,而getTextBounds计算给定文本适合的最小边界。

希望这个结果对你有用。

测试代码:

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

我的经验是getTextBounds将返回封装文本的绝对最小边界矩形,而不一定是渲染时使用的测量宽度。我还想说measureText假设一行。

为了获得准确的测量结果,你应该使用StaticLayout来渲染文本并提取测量值。

例如:

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

这个解决方案在确定最小宽度方面不是100%准确。

我还在研究如何测量画布上的文本。在阅读了老鼠的文章后,我对如何测量多行文本有了一些问题。从这些贡献中没有明显的方法,但经过一些研究后,我看到了StaticLayout类。它允许您测量多行文本(带有“\n”的文本),并通过相关的Paint配置文本的更多属性。

下面是一个演示如何测量多行文本的代码片段:

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

wrapwitdh能够确定是否要将多行文本限制在一定的宽度。

由于StaticLayout.getWidth()只返回这个boundedWidth,你必须采取另一个步骤来获得你的多行文本所需的最大宽度。你可以确定每条线的宽度,最大宽度当然是最高的线宽:

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

很抱歉再次回答这个问题…我需要嵌入图像。

我认为@老鼠发现的结果是错误的。对于字体大小为60的字体,观察结果可能是正确的,但当文本更小时,它们就会变得更不一样。10 px。在这种情况下,文本实际上超出了边界。

enter image description here

截图源代码:

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

老鼠的回答很好…下面是对真正问题的描述:

简单的回答是Paint.getTextBounds(String text, int start, int end, Rect bounds)返回Rect,它不是从(0,0)开始的。也就是说,为了获得实际的文本宽度,通过调用getTextBounds()中的相同 Paint对象Canvas.drawText(String text, float x, float y, Paint paint)来设置,你应该添加Rect的左侧位置。如下所示:

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

注意这个bounds.left -这是问题的关键。

通过这种方式,您将接收到与使用Canvas.drawText()接收到的文本宽度相同的文本。

同样的函数应该用于获取文本的height:

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

附注:我没有测试这段确切的代码,但测试了这个概念。


更详细的解释在 answer中给出。

还有另一种方法来精确测量文本边界,首先你应该获得当前绘画和文本的路径。 在你的例子中,它应该是这样的:

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

之后你可以调用:

mPath.computeBounds(mBoundsPath, true);
在我的代码中,它总是返回正确的和期望的值。 但是,不确定它是否比你的方法更快

这就是我如何计算第一个字母的实际尺寸(你可以改变方法头以适应你的需要,即用String代替char[]):

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

注意,我使用TextPaint而不是原来的油漆类。

下面的图片描述了getTextBoundsmeasureText之间的区别。

简而言之,

  1. getTextBounds是获取精确文本的RECT。measureText是文本的长度,包括左边和右边的额外空白。

  2. 如果文本之间有空格,则在measureText中测量,但不包括在TextBounds的长度中,尽管坐标会发生移位。

  3. 文本可以向左倾斜(歪斜)。在这种情况下,左边的边界框将超出measureText的测量值,并且文本边界的总长度将大于measureText

  4. 文本可以向右倾斜(歪斜)。在这种情况下,边界框右侧将超出measureText的测量值,并且文本边界的总长度将大于measureText

enter image description here