Androidペイント:.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

[my way] [1]が見えます。それは位置と正しい境界を得ることができます。[1]:stackoverflow.com/a/19979937/1621354
Alex Chi

この質問への他の訪問者のテキスト測定する方法に関する関連説明
Suragch 2017

回答:


370

あなたは私がそのような問題を検査するためにしたことをすることができます:

AndroidソースコードのPaint.javaソースを調べ、measureTextメソッドとgetTextBoundsメソッドの両方を確認します。C ++で実装されているネイティブメソッドである、measureTextがnative_measureTextを呼び出し、getTextBoundsがnativeGetStringBoundsを呼び出すことを学びます。

したがって、両方を実装する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の呼び出しで発生します。

これら2つの呼び出しの計算された幅の差は最大で1です。

編集4 2011年10月

視覚化よりも優れている可能性があります。私は自分自身の探求のために、そして賞金に値するために努力をしました:)

ここに画像の説明を入力してください

これはフォントサイズ60で、赤は境界の長方形、紫はmeasureTextの結果です。

境界の左の部分は左からいくつかのピクセルで始まり、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);
  }

3
コメントが長くてごめんなさい、忙しい一週間でした。C ++コードを掘り下げていただきありがとうございます。残念ながら、その差は1より大きく、丸められた値を使用する場合でも発生します。たとえば、テキストサイズを29.0に設定すると、measureText()= 263およびgetTextBounds()。width = 260
slezica

問題を再現できるペイントの設定と測定を含む最小限のコードを投稿してください。
ポインタヌル

更新しました。遅れて申し訳ありません。
slezica

私はここでリッチと一緒です。すばらしい研究!賞金は完全に値し、授与されました。お忙しいところありがとうございました!
slezica

16
@マウス私はこの質問をもう一度見つけました(私はOPです)。私はあなたの答えがどれほど信じられないかを忘れていました。今後ともよろしくお願いいたします。私が投資したベスト100rp!
slezica

21

これに関する私の経験ではgetTextBounds、テキストをカプセル化する絶対最小境界四角形が返されます。必ずしもレンダリング時に使用される測定幅ではありません。また、measureText1行と想定しています。

正確な測定結果を取得するには、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

幅に関して私の悪い。1行にレンダリングされるテキストの幅がmeasureText必要な場合は、で問題なく動作するはずです。それ以外の場合、幅の制約(onMeasureまたはなど)がわかっている場合は、onLayout上記で投稿したソリューションを使用できます。テキストのパーチャンスを自動サイズ変更しようとしていますか?また、テキストの境界が常に少し小さい理由は、文字のパディングが除外されているためです。 、描画に必要な最小の境界付き四角形
2011年

私は確かにTextViewの自動サイズ変更を試みています。私も身長が必要です、それが私の問題が始まったところです!それ以外の場合、measureWidth()は正常に動作します。その後、異なるアプローチが異なる値を与える理由について
知りました

1
また、ラップして複数行であるテキストビューは、match_parentで必要な幅だけでなく幅も測定するため、上記の幅パラメーターがStaticLayoutに対して有効になることにも注意してください。テキストのリサイザが必要な場合は、stackoverflow.com
Chase

私はテキスト拡張アニメーションが必要でした、そしてこれはたくさんを助けました!ありがとうございました!
ボックス

18

マウスの答えは素晴らしいです...そして、これが本当の問題の説明です:

短い簡単な答えは、で始まらないPaint.getTextBounds(String text, int start, int end, Rect bounds)リターンです。つまり、getTextBounds()から同じ Paintオブジェクトで呼び出して設定されるテキストの実際の幅を取得するには、Rectの左の位置を追加する必要があります。そんな感じ:Rect(0,0)Canvas.drawText(String text, float x, float y, Paint paint)

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

Ps:私はこの正確なコードをテストしませんでしたが、概念をテストしました。


この回答では、より詳細な説明が提供されています。


2
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は範囲への参照です)
Nulano

11

その質問にもう一度回答して申し訳ありません...画像を埋め込む必要がありました。

@miceで見つかった結果は誤解を招くと思います。フォントサイズが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 );
    }
  }

ああ、ミステリーは引きずる。好奇心がなくても、私はまだこれに興味があります。他に何かありましたらお知らせください。
slezica

測定したいフォントサイズに20の係数を掛けて、結果をそれで割ると、問題は少し改善されます。安全のために、測定サイズの幅を1〜2%増やすこともできます。最終的には、長いと測定されたテキストが表示されますが、少なくともテキストのオーバーラップはありません。
Moritz

6
バウンディングrectは幅や高さではなく、rectのように返されるため、テキストを境界rectの正確な内側に描画するには、X = 0.0fでテキストを描画しないでください。正しく描画するには、「-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;
}

igetLineWidthに渡すべきではありません(毎回0を渡しています)
Rich S

2

テキストの境界を正確に測定する別の方法があります。最初に、現在のペイントとテキストのパスを取得する必要があります。あなたの場合は次のようになります:

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

その後、あなたは呼び出すことができます:

mPath.computeBounds(mBoundsPath, true);

私のコードでは、常に正しい期待値を返します。しかし、それがあなたのアプローチよりも速く機能するかどうかはわかりません。


テキストの周りにストロークの輪郭を描くためにとにかくgetTextPathを行っていたので、これは便利だと思いました。
ToolmakerSteve

1

との違いはgetTextBoundsmeasureText下の画像で説明されています。

要するに、

  1. getTextBounds正確なテキストのRECTを取得することです。measureText左右の余分な隙間を含む、テキストの長さです。

  2. テキスト間にスペースがある場合measureText、座標はシフトされますが、TextBoundsの長さは含まれますが、含まれません。

  3. テキストを左に傾ける(スキュー)ことができます。この場合、境界ボックスの左側は、measureTextの測定値の外側を超え、バインドされたテキストの全長は、measureText

  4. テキストを右に傾ける(スキュー)ことができます。この場合、境界ボックスの右側は、measureTextの測定値の外側を超え、バインドされたテキストの全長は、measureText

ここに画像の説明を入力してください


0

これは私が最初の文字の実際の寸法を計算した方法です(メソッドヘッダーをニーズに合わせて変更できます。つまり、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;
}

元のPaintクラスの代わりにTextPaintを使用していることに注意してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.