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


191

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

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

您可以看到[我的方式] [1]。那可以得到位置和正确的界限。[1]:stackoverflow.com/a/19979937/1621354
Alex Chi

有关为该问题的其他访问者测量文本的方法的相关说明
Suragch

Answers:


370

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

学习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的过程中出现舍入错误,并且这种情况发生在调用函数SkRect :: roundOut的SkPaintGlue :: doTextBounds中的Paint.cpp中。

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

编辑2011年10月4日

什么可能比可视化更好。我付出了努力,进行了自己的探索,并得到了应有的赏金:)

在此处输入图片说明

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

可以看到,左边的边界从左边开始一些像素,而measureText的值在左右两侧都增加了该值。这就是所谓的字形的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);
  }

3
很抱歉花这么长时间发表评论,我忙了一个星期。感谢您抽出宝贵的时间来研究C ++代码!不幸的是,差异大于1,甚至在使用舍入值时也会发生-例如,将文本大小设置为29.0会导致measureText()= 263和getTextBounds()。width = 260
slezica 2011年

请在Paint设置和测量中发布最少的代码,以重现该问题。
指针为空

更新。抱歉,再次延迟。
slezica 2011年

我和里奇在一起。伟大的研究!赏金完全应得并获得奖励。感谢您的时间和精力!
slezica 2011年

16
@mice我刚刚再次发现了这个问题(我是OP)。我忘记了你的答案是多么的不可思议。感谢未来!我投资的最好的100rp!
slezica

21

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

不知道StaticLayout!getWidth()无法正常工作,它只会返回我在构造函数中传递的内容(width不是maxWidth),但是getLineWith()可以。我也找到了静态方法getDesiredWidth(),尽管没有等效的高度。请更正答案!另外,我真的很想知道为什么这么多不同的方法会产生不同的结果。
slezica 2011年

我对宽度不好。如果您希望将某些文本的宽度显示在一行上,则measureText应该可以正常工作。否则,如果您知道宽度限制(例如in onMeasure或)onLayout,则可以使用我在上面发布的解决方案。是否要尝试自动调整文本大小的大小?而且,文本边界总是略小是因为它不包含任何字符填充,只是最小的有界矩形需要进行绘图。
大通

我确实在尝试自动调整TextView的大小。我也需要身高,这就是我的问题开始的地方!measureWidth()可以正常工作。之后,我很好奇为什么不同的方法赋予了不同的价值
slezica 2011年

1
还需要注意的一件事是,包装并为多行的textview将在match_parent处测量宽度,而不仅仅是所需宽度,这使上述对StaticLayout的宽度参数有效。如果你需要一个文本缩放器,看看我的帖子在stackoverflow.com/questions/5033012/...
大通

我需要一个文本扩展动画,这帮了很多忙!谢谢!

18

老鼠的回答很棒……这是实际问题的描述:

简短的简单答案是Paint.getTextBounds(String text, int start, int end, Rect bounds)返回的Rect开头不是(0,0)。也就是说,要获取将通过Canvas.drawText(String text, float x, float y, Paint paint)使用相同的 Paint对象从getTextBounds()进行调用来设置的文本的实际宽度,应添加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;
}

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


答案给出了更多详细的解释。


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

11

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

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

啊,迷雾继续。即使出于好奇,我仍然对此感兴趣。如果您还有其他问题,请告诉我!
slezica 2012年

当您将要测量的字体大小乘以例如20的系数,然后将结果除以该系数时,可以稍微解决此问题。为了安全起见,您可能还想将测量尺寸的宽度增加1-2%。最后,您将得到长度很长的文本,但至少没有文本重叠。
莫里茨2012年

6
要精确地在边界矩形内绘制文本,您不得绘制X = 0.0f的文本,因为边界矩形的返回方式类似于rect,而不是宽度和高度。为了正确绘制它,您应该在“ -bounds.left”值上偏移X坐标,然后它将正确显示。
罗马

10

免责声明:就确定最小宽度而言,此解决方案并非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;
}

您不应该传递i给getLineWidth(您每次都传递0)吗?
Rich S

2

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

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

之后,您可以致电:

mPath.computeBounds(mBoundsPath, true);

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


我发现这很有用,因为无论如何我都在执行getTextPath,以便在文本周围绘制笔触轮廓。
ToolmakerSteve

1

getTextBounds和之间的区别measureText如下图所示。

简而言之,

  1. getTextBounds是获得确切文本的RECT。的measureText是文本的长度,包括左,右额外的差距。

  2. 如果文本之间有空格measureText,尽管坐标会发生偏移,但它会以TextBounds的长度(但不包括在内)进行测量。

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

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

在此处输入图片说明


0

这就是我计算第一个字母的实际尺寸的方式(您可以更改方法标题以适合您的需要,即代替char[]use 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类。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.