Android 画图:.measureText() 与 .getTextBounds()

Posted

技术标签:

【中文标题】Android 画图:.measureText() 与 .getTextBounds()【英文标题】:Android Paint: .measureText() vs .getTextBounds() 【发布时间】:2011-11-24 20:07:16 【问题描述】:

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

令我惊讶的是,我测试了.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

【问题讨论】:

你可以看到[我的方式][1]。这可以得到位置和正确的界限。 [1]:***.com/a/19979937/1621354 Related explanation about the methods of measuring text 此问题的其他访问者。 【参考方案1】:

您可以按照我的做法检查此类问题:

学习 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 日编辑

什么可能比可视化更好。我付出了努力,为了自己的探索,为了值得赏金:)

这是60号字体,红色是bounds矩形,紫色是measureText的结果。

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

所以测试的结果是 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);
  

【讨论】:

抱歉花了这么长时间发表评论,我这周很忙。感谢您花时间深入研究 C++ 代码!不幸的是,差异大于 1,即使使用舍入值也会发生 - 例如将文本大小设置为 29.0 会导致 measureText()=263 和 getTextBounds().width=260 请发布带有 Paint 设置和测量的最少代码,这样可以重现问题。 已更新。再次抱歉耽搁了。 我和 Rich 在一起。伟大的研究!赏金当之无愧并获得奖励。感谢您的时间和精力! @mice 我刚刚又发现了这个问题(我是 OP)。我忘记了你的回答有多不可思议。来自未来的感谢!我投资过的最好的 100rp!【参考方案2】:

我的经验是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();

【讨论】:

不知道静态布局! getWidth() 不起作用,它只返回我在 costructor 中传递的内容(即width,而不是maxWidth),但 getLineWith() 会。我还找到了静态方法 getDesiredWidth(),虽然没有等价的高度。请更正答案!另外,我真的很想知道为什么这么多不同的方法会产生不同的结果。 我对宽度不好。如果您想要将在一行上呈现的某些文本的宽度,measureText 应该可以正常工作。否则,如果您知道宽度约束(例如在onMeasureonLayout 中,您可以使用我在上面发布的解决方案。您是否尝试自动调整文本大小?另外,文本边界总是略小的原因是因为它不包括任何字符填充,只是绘图所需的最小有界矩形。 我确实在尝试自动调整 TextView 的大小。我也需要身高,这就是我的问题开始的地方! measureWidth() 否则工作正常。在那之后,我很好奇为什么不同的方法给出不同的值 还有一点需要注意的是,一个包含多行的文本视图将在 match_parent 处测量宽度,而不仅仅是所需的宽度,这使得 StaticLayout 的上述宽度参数有效。如果您需要文本调整器,请查看我在 ***.com/questions/5033012/… 的帖子 我需要一个文本扩展动画,这帮了很多忙!谢谢!【参考方案3】:

老鼠的答案很棒......这是对真正问题的描述:

简短的回答是Paint.getTextBounds(String text, int start, int end, Rect bounds) 返回Rect,它不是从(0,0) 开始的。也就是说,要获得将通过使用 getTextBounds() 中的 same 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;

P.s.:我没有测试这个确切的代码,而是测试了这个概念。


this 回答中给出了更详细的解释。

【讨论】:

Rect 定义 (b.width) 为 (b.right-b.left) 和 (b.height) 为 (b.bottom-b.top)。因此,您可以将 (b.left+b.width) 替换为 (b.right),将 (b.bottom+b.height) 替换为 (2*b.bottom-b.top),但我认为您想要 ( b.bottom-b.top) 代替,与 (b.height) 相同。我认为您对宽度是正确的(基于检查 C++ 源代码), getTextWidth() 返回与 (b.right) 相同的值,可能存在一些舍入错误。 (b 是对边界的引用)【参考方案4】:

很抱歉再次回答该问题...我需要嵌入图片。

我认为@mice 发现的结果具有误导性。对于 60 的字体大小,观察结果可能是正确的,但当文本较小时,它们会变得更加不同。例如。 10像素。在这种情况下,文本实际上是在边界之外绘制的。

截图源代码:

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

【讨论】:

啊,谜团还在继续。即使出于好奇,我仍然对此感兴趣。如果您发现任何其他问题,请告诉我! 当您将要测量的字体大小乘以一个因子(比如说 20)然后将结果除以该因子时,可以稍微解决这个问题。为了安全起见,您可能还想在测量尺寸的基础上多增加 1-2% 的宽度。最后,您将获得按长度测量的文本,但至少没有文本重叠。 要在边界矩形内精确绘制文本,您不能使用 X=0.0f 绘制文本,因为边界矩形返回的是矩形,而不是宽度和高度。要正确绘制它,您应该在“-bounds.left”值上偏移 X 坐标,然后它将正确显示。【参考方案5】:

免责声明:此解决方案在确定最小宽度方面并非 100% 准确。

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

这是一个显示如何测量多行文本的 sn-p:

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;

【讨论】:

你不应该将i 传递到getLineWidth 中吗(你每次只传递0)【参考方案6】:

getTextBoundsmeasureText 的区别如下图所示。

总之,

    getTextBounds 是获取准确文本的 RECT。 measureText 是文本的长度,包括左右多余的间隙。

    如果文本之间有空格,则以measureText 衡量,但不包括TextBounds 的长度,尽管坐标会发生偏移。

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

    文本可以向右倾斜(倾斜)。在这种情况下,边界框右侧会超出measureText的测量范围,文本边界的总长度会大于measureText

【讨论】:

【参考方案7】:

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

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

之后你可以调用:

mPath.computeBounds(mBoundsPath, true);

在我的代码中,它总是返回正确的预期值。 但是,不确定它是否比您的方法更快。

【讨论】:

我发现这很有用,因为我一直在做 getTextPath,以便在文本周围绘制笔触轮廓。【参考方案8】:

这就是我计算第一个字母的实际尺寸的方法(您可以更改方法标题以满足您的需要,即使用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 类。

【讨论】:

以上是关于Android 画图:.measureText() 与 .getTextBounds()的主要内容,如果未能解决你的问题,请参考以下文章

画布不正确的 measureText()

canvas的measureText()方法

LinkLabel 需要比 TextRenderer.MeasureText 更多的空间说

SKIA - measureText() 返回的值不准确

Chrome canvas 2d context measureText给了我奇怪的结果

[js高手之路] html5 canvas系列教程 - 文本样式(strokeText,fillText,measureText,textAlign,textBaseline)