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

我使用Paint.getTextBounds()来测量文本,因为我有兴趣获取要呈现的文本的高度和宽度。 但是,呈现的实际文本总是比由.width()填充的Rect信息的.width()宽一些。

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

为什么他们报告不同的宽度? 我怎样才能正确地获得高度和宽度? 我的意思是,我可以使用.measureText() ,但是我不知道是否应该信任由.height()返回的getTextBounds()

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

 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 

你可以做我所做的检查这样的问题:

学习Android源代码,Paint.java源码,请参阅measureText和getTextBounds方法。 您将了解到measureText调用native_measureText,getTextBounds调用nativeGetStringBounds,这是在C ++中实现的本地方法。

所以你会继续学习Paint.cpp,它实现了两者。

native_measureText – > SkPaintGlue :: measureText_CII

nativeGetStringBounds – > SkPaintGlue :: getStringBounds

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

进一步深入Skia,我看到两个调用在同一个函数中导​​致了相同的计算,只是返回结果不同。

回答你的问题:你的电话都做同样的计算。 结果可能的差异在于, getTextBounds将边界作为整数返回,而measureText返回float值。

所以你得到的是在将float转换为int的过程中出现四舍五入误差,这发生在调用函数SkRect :: roundOut的SkPaintGlue :: doTextBounds中的Paint.cpp中。

这两个调用的计算宽度之间的差异可能最大为1。

编辑2011年10月4日

什么可能比可视化更好。 我付出了努力,为自己的探索,并为此应得的赏赐:) 在这里输入图像描述

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

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

所以testing的结果是,measureText在文本两端添加了一些提前值,而getTextBounds计算给定文本适合的最小边界。

希望这个结果对你有用。

testing代码:

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

老鼠的答案是伟大的…这里是真正的问题的描述:

简单的回答是, Paint.getTextBounds(String text, int start, int end, Rect bounds)返回不是从(0,0)开始的Rect 。 也就是说,要通过调用Canvas.drawText(String text, float x, float y, Paint paint)来设置文本的实际宽度,使用Canvas.drawText(String text, float x, float y, Paint 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()相同的文本宽度。

同样的function应该是获得文本的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; } 

Ps:我没有testing这个确切的代码,但是testing了这个概念。


在这个答案中给出了更详细的解释。

抱歉在这个问题上再次回答…我需要embedded图像。

我认为@micefind的结果是有误导性的。 对于60的字体大小的观察可能是正确的,但是当文字较小时它们会变得更加不同。 例如。 10px的。 在这种情况下,文本实际上是超越界限的。

在这里输入图像描述

屏幕截图的源代码:

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

免责声明:这个解决scheme在确定最小宽度方面不是100%准确的。

我也在搞清楚如何在canvas上测量文字。 从老鼠看完这篇文章之后,我对如何测量多行文本有一些疑问。 从这些贡献没有明显的方式,但经过一些研究,我凸轮StaticLayout类。 它允许你测量多行文本(带有“\ n”的文本),并通过关联的Paintconfiguration更多的文本属性。

以下是显示如何测量多行文本的代码片段:

 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()只返回这个有界的宽度,所以你必须采取另一个步骤来获得多行文本所需的最大宽度。 您可以确定每条线的宽度,最大宽度当然是最高线宽:

 private float getMaxLineWidth( StaticLayout layout ) { float maxLine = 0.0f; int lineCount = layout.getLineCount(); for( int i = 0; i < lineCount; i++ ) { if( layout.getLineWidth( 0 ) > maxLine ) { maxLine = layout.getLineWidth( 0 ); } } return maxLine; } 

还有另外一种方法可以精确地测量文本边界,首先应该获取当前Paint和文本的path。 你的情况应该是这样的:

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

之后,你可以打电话给:

 mPath.computeBounds(mBoundsPath, true); 

在我的代码中,它总是返回正确的和期望的值。 但是,不知道它是否比你的方法更快。

这是我如何计算第一个字母的真实尺寸(你可以改变方法头来适应你的需要,而不是char[]使用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; } 

请注意,我正在使用TextPaint而不是原始的Paint类。