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

191

Ich messe Text mit Paint.getTextBounds(), da ich daran interessiert bin, sowohl die Höhe als auch die Breite des zu rendernden Textes zu ermitteln. Der tatsächlich gerenderte Text ist jedoch immer etwas breiter als die .width()von Rectausgefüllten Informationen getTextBounds().

Zu meiner Überraschung habe ich getestet .measureText()und festgestellt, dass es einen anderen (höheren) Wert zurückgibt. Ich habe es versucht und fand es richtig.

Warum melden sie unterschiedliche Breiten? Wie kann ich die Höhe und Breite richtig ermitteln? Ich meine, ich kann verwenden .measureText(), aber dann würde ich nicht wissen, ob ich dem .height()zurückgegebenen von vertrauen sollte getTextBounds().

Wie angefordert, ist hier minimaler Code, um das Problem zu reproduzieren:

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

Die Ausgabe zeigt, dass der Unterschied nicht nur größer als 1 wird (und kein Rundungsfehler in letzter Minute ist), sondern auch mit der Größe zuzunehmen scheint (ich wollte mehr Schlussfolgerungen ziehen, aber er kann vollständig von der Schriftart abhängig sein):

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
Slezica
quelle
Sie können [meinen Weg] [1] sehen. Das kann die Position bekommen und richtig binden. [1]: stackoverflow.com/a/19979937/1621354
Alex Chi
Verwandte Erläuterungen zu den Methoden zum Messen von Text für andere Besucher dieser Frage.
Suragch

Antworten:

370

Sie können das tun, was ich getan habe, um ein solches Problem zu untersuchen:

Studieren Sie den Android-Quellcode, Paint.java-Quelle, und sehen Sie sich sowohl die MeasureText- als auch die GetTextBounds-Methode an. Sie werden erfahren, dass MeasureText native_measureText aufruft und getTextBounds nativeGetStringBounds aufruft, native Methoden, die in C ++ implementiert sind.

Sie würden also weiterhin Paint.cpp studieren, das beide implementiert.

native_measureText -> SkPaintGlue :: MeasureText_CII

nativeGetStringBounds -> SkPaintGlue :: getStringBounds

Jetzt prüft Ihre Studie, wo sich diese Methoden unterscheiden. Nach einigen Parameterprüfungen rufen beide die Funktion SkPaint :: MeasureText in Skia Lib (Teil von Android) auf, aber beide rufen unterschiedliche überladene Form auf.

Wenn ich mich weiter mit Skia befasse, sehe ich, dass beide Aufrufe zu derselben Berechnung in derselben Funktion führen und nur unterschiedliche Ergebnisse zurückgeben.

So beantworten Sie Ihre Frage: Beide Anrufe führen dieselbe Berechnung durch. Ein möglicher Unterschied des Ergebnisses besteht darin, dass getTextBounds Grenzen als Ganzzahl zurückgibt , während MeasureText den Gleitkommawert zurückgibt.

Sie erhalten also einen Rundungsfehler während der Konvertierung von float in int. Dies geschieht in Paint.cpp in SkPaintGlue :: doTextBounds beim Aufruf der Funktion SkRect :: roundOut.

Der Unterschied zwischen der berechneten Breite dieser beiden Anrufe kann maximal 1 betragen.

BEARBEITEN 4. Oktober 2011

Was kann besser sein als Visualisierung. Ich habe mir die Mühe gemacht, selbst zu erkunden und Kopfgeld zu verdienen :)

Geben Sie hier die Bildbeschreibung ein

Dies ist die Schriftgröße 60, in Rot ist das Rechteck begrenzt , in Lila ist das Ergebnis von MeasureText.

Es ist zu sehen, dass der linke linke Teil einige Pixel von links beginnt und der Wert von MeasureText sowohl links als auch rechts um diesen Wert erhöht wird. Dies wird als AdvanceX-Wert von Glyph bezeichnet. (Ich habe dies in Skia-Quellen in SkPaint.cpp entdeckt.)

Das Ergebnis des Tests ist also, dass MeasureText dem Text auf beiden Seiten einen Vorabwert hinzufügt, während getTextBounds minimale Grenzen berechnet, an die der angegebene Text passt.

Hoffe, dieses Ergebnis ist nützlich für Sie.

Testcode:

  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);
  }
Zeiger Null
quelle
3
Es tut mir leid, dass ich so lange gebraucht habe, um einen Kommentar abzugeben. Ich hatte eine arbeitsreiche Woche. Vielen Dank, dass Sie sich die Zeit genommen haben, sich mit dem C ++ - Code zu befassen! Leider ist der Unterschied größer als 1 und tritt auch dann auf, wenn gerundete Werte verwendet werden. Wenn Sie beispielsweise die Textgröße auf 29,0 setzen, erhalten Sie MeasureText () = 263 und GetTextBounds ().
Width
Bitte geben Sie minimalen Code mit Paint-Setup und -Messung an, der das Problem reproduzieren kann.
Zeiger Null
Aktualisiert. Entschuldigung nochmal für die Verzögerung.
Slezica
Ich bin hier mit Rich. Tolle Forschung! Kopfgeld voll verdient und vergeben. Vielen Dank für Ihre Zeit und Mühe!
Slezica
16
@ Mäuse Ich habe diese Frage gerade wieder gefunden (ich bin der OP). Ich hatte vergessen, wie unglaublich deine Antwort war. Danke aus der Zukunft! Beste 100rp, die ich investiert habe!
Slezica
21

Meine Erfahrung damit ist, getTextBoundsdass das absolut minimale Begrenzungsrecht zurückgegeben wird, das den Text einkapselt, nicht unbedingt die gemessene Breite, die beim Rendern verwendet wird. Ich möchte auch sagen, dass dies measureTexteine Zeile voraussetzt.

Um genaue Messergebnisse zu erhalten, sollten Sie die verwenden StaticLayout den Text mit rendern und die Messungen herausziehen.

Beispielsweise:

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();
Verfolgungsjagd
quelle
Wusste nichts über StaticLayout! getWidth () funktioniert nicht, es gibt nur das zurück, was ich im Costructor übergeben habe (was widthnicht der Fall ist maxWidth), aber getLineWith () wird es tun. Ich habe auch die statische Methode getDesiredWidth () gefunden, obwohl es kein Höhenäquivalent gibt. Bitte korrigieren Sie die Antwort! Außerdem würde ich gerne wissen, warum so viele verschiedene Methoden zu unterschiedlichen Ergebnissen führen.
Slezica
Mein schlechtes in Bezug auf die Breite. Wenn Sie die Breite eines Textes möchten, der in einer einzelnen Zeile gerendert wird, sollte dies einwandfrei measureTextfunktionieren. Andernfalls, wenn Sie die Breitenkonstanten kennen (z. B. in onMeasureoder onLayout, können Sie die oben beschriebene Lösung verwenden. Versuchen Sie, die Textgröße automatisch zu ändern? Der Grund dafür, dass die Textgrenzen immer etwas kleiner sind, liegt darin, dass keine Zeichenauffüllung vorhanden ist , nur das kleinste begrenzte Rechteck, das zum Zeichnen benötigt wird.
Chase
Ich versuche in der Tat, die Größe einer Textansicht automatisch zu ändern. Ich brauche auch die Höhe, da haben meine Probleme angefangen! MeasureWidth () funktioniert ansonsten einwandfrei. Danach wurde ich neugierig, warum verschiedene Ansätze unterschiedliche Werte
ergaben
1
Zu beachten ist auch, dass eine Textansicht, die umbrochen und mehrzeilig ist, bei match_parent die Breite und nicht nur die erforderliche Breite misst, wodurch der obige Parameter width für StaticLayout gültig wird. Wenn Sie einen Text Resizer benötigen, lesen Sie meinen Beitrag unter stackoverflow.com/questions/5033012/…
Chase
Ich brauchte eine Texterweiterungsanimation und das half einem Haufen! Danke dir!
Box
18

Die Antwort der Mäuse ist großartig ... Und hier ist die Beschreibung des eigentlichen Problems:

Die kurze einfache Antwort lautet: Paint.getTextBounds(String text, int start, int end, Rect bounds)Rückkehr, Rectdie nicht bei beginnt (0,0). Das heißt, um die tatsächliche Breite des Texts zu erhalten, der durch Aufrufen Canvas.drawText(String text, float x, float y, Paint paint)mit demselben Paint-Objekt von getTextBounds () festgelegt wird, sollten Sie die linke Position von Rect hinzufügen. Sowas in der Art:

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

Beachten Sie dies bounds.left- dies ist der Schlüssel des Problems.

Auf diese Weise erhalten Sie die gleiche Textbreite, die Sie mit verwenden würden Canvas.drawText().

Und die gleiche Funktion sollte für das Abrufen heightdes Textes sein:

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: Ich habe diesen genauen Code nicht getestet, aber die Konzeption getestet.


Diese Antwort enthält eine viel detailliertere Erklärung .

Prizoff
quelle
2
Rect definiert (b.width) als (b.right-b.left) und (b.height) als (b.bottom-b.top). Daher können Sie (b.left + b.width) durch (b.right) und (b.bottom + b.height) durch (2 * b.bottom-b.top) ersetzen, aber ich denke, Sie möchten ( b.bottom-b.top), was dasselbe ist wie (b.height). Ich denke, Sie haben Recht mit der Breite (basierend auf der Überprüfung der C ++ - Quelle). GetTextWidth () gibt den gleichen Wert wie (b.right) zurück. Möglicherweise gibt es Rundungsfehler. (b ist ein Verweis auf Grenzen)
Nulano
11

Entschuldigung für die erneute Beantwortung dieser Frage ... Ich musste das Bild einbetten.

Ich denke, die Ergebnisse, die @mice gefunden hat, sind irreführend. Die Beobachtungen mögen für die Schriftgröße von 60 korrekt sein, aber sie werden viel anders, wenn der Text kleiner ist. Z.B. 10px. In diesem Fall wird der Text tatsächlich ÜBER die Grenzen hinaus gezeichnet.

Geben Sie hier die Bildbeschreibung ein

Quellcode des Screenshots:

  @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 );
    }
  }
Moritz
quelle
Ah, das Geheimnis zieht sich hin. Das interessiert mich immer noch, auch wenn ich neugierig bin. Lassen Sie mich wissen, wenn Sie etwas anderes herausfinden, bitte!
Slezica
Das Problem kann ein wenig behoben werden, wenn Sie die zu messende Schriftgröße mit einem Faktor von beispielsweise 20 multiplizieren und dann das Ergebnis dadurch dividieren. DANN möchten Sie möglicherweise auch 1-2% mehr Breite der gemessenen Größe hinzufügen, um auf der sicheren Seite zu sein. Am Ende haben Sie Text, der zu lang gemessen wird, aber zumindest gibt es keinen überlappenden Text.
Moritz
6
Um den Text genau innerhalb des Begrenzungsrektors zu zeichnen, dürfen Sie keinen Text mit X = 0.0f zeichnen, da der Begrenzungsrechteck wie ein Rechteck zurückkehrt, nicht wie eine Breite und Höhe. Um es richtig zu zeichnen, sollten Sie die X-Koordinate auf dem Wert "-bounds.left" versetzen, dann wird sie korrekt angezeigt.
Roman
10

HAFTUNGSAUSSCHLUSS: Diese Lösung ist hinsichtlich der Bestimmung der minimalen Breite nicht 100% genau.

Ich habe auch herausgefunden, wie man Text auf einer Leinwand misst. Nachdem ich den großartigen Beitrag von Mäusen gelesen hatte, hatte ich einige Probleme beim Messen von mehrzeiligem Text. Es gibt keinen offensichtlichen Weg von diesen Beiträgen, aber nach einigen Recherchen komme ich durch die StaticLayout-Klasse. Sie können mehrzeiligen Text (Text mit "\ n") messen und über die zugehörige Farbe viel mehr Eigenschaften Ihres Textes konfigurieren.

Hier ist ein Ausschnitt, der zeigt, wie mehrzeiliger Text gemessen wird:

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

Der Wrapwitdh kann bestimmen, ob Sie Ihren mehrzeiligen Text auf eine bestimmte Breite beschränken möchten.

Da StaticLayout.getWidth () nur diese begrenzte Breite zurückgibt, müssen Sie einen weiteren Schritt ausführen, um die maximale Breite zu erhalten, die für Ihren mehrzeiligen Text erforderlich ist. Sie können jede Linienbreite bestimmen und die maximale Breite ist natürlich die höchste Linienbreite:

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;
}
Moritz
quelle
sollten Sie nicht iin die getLineWidth gehen (Sie gehen jedes Mal nur 0)
Rich S
2

Es gibt eine andere Möglichkeit, die Textgrenzen genau zu messen. Zuerst sollten Sie den Pfad für die aktuelle Farbe und den aktuellen Text ermitteln. In Ihrem Fall sollte es so sein:

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

Danach können Sie anrufen:

mPath.computeBounds(mBoundsPath, true);

In meinem Code werden immer korrekte und erwartete Werte zurückgegeben. Aber nicht sicher, ob es schneller funktioniert als Ihr Ansatz.

römisch
quelle
Ich fand das nützlich, da ich sowieso getTextPath machte, um einen Strichumriss um den Text zu zeichnen.
ToolmakerSteve
1

Der Unterschied zwischen getTextBoundsund measureTextwird mit dem Bild unten beschrieben.

Zusamenfassend,

  1. getTextBoundsist es, die RECT des genauen Textes zu erhalten. Das measureTextist die Länge des Textes, einschließlich der zusätzlichen Lücke links und rechts.

  2. Wenn zwischen dem Text Leerzeichen vorhanden sind, wird dies measureTextin der Länge der TextBounds gemessen, jedoch nicht berücksichtigt, obwohl die Koordinaten verschoben werden.

  3. Der Text kann nach links geneigt werden. In diesem Fall würde der Begrenzungsrahmen auf der linken Seite außerhalb des Maßes für den MeasureText liegen, und die Gesamtlänge des gebundenen Textes wäre größer alsmeasureText

  4. Der Text kann nach rechts geneigt werden. In diesem Fall würde der Begrenzungsrahmen auf der rechten Seite außerhalb der Messung des MeasureText liegen, und die Gesamtlänge des gebundenen Textes wäre größer alsmeasureText

Geben Sie hier die Bildbeschreibung ein

Elye
quelle
0

So habe ich die tatsächlichen Abmessungen für den ersten Buchstaben berechnet (Sie können den Methodenkopf an Ihre Bedürfnisse anpassen, dh anstatt ihn zu char[]verwenden 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;
}

Beachten Sie, dass ich TextPaint anstelle der ursprünglichen Paint-Klasse verwende.

milosmns
quelle