wake-up-neo.com

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

Ich messe Text mit Paint.getTextBounds(), da ich daran interessiert bin, sowohl die Höhe als auch die Breite des zu rendernden Texts zu ermitteln. Der tatsächlich wiedergegebene Text ist jedoch immer etwas breiter als die .width() der Rect -Informationen, die von getTextBounds() ausgefüllt werden.

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

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

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 sich auch mit der Größe zu erhöhen scheint (ich wollte mehr Schlussfolgerungen ziehen, aber es kann völlig von der Schriftart abhängen):

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
186
slezica

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

Study Android Quellcode, Paint.Java-Quelle, siehe MeasureText-Methode und GetTextBounds-Methode. Sie werden feststellen, dass MeasureText native_measureText und GetTextBounds nativeGetStringBounds-Methoden aufruft, die in C++ implementiert sind.

Sie werden 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 Parameterkontrollen rufen beide die Funktion SkPaint :: measureText in Skia Lib (Teil von Android) auf, aber beide rufen unterschiedliche überladene Formulare auf.

Wenn ich mich weiter mit Skia befasse, sehe ich, dass beide Aufrufe in derselben Funktion zur selben Berechnung führen und nur ein anderes Ergebnis zurückgeben.

m Ihre Frage zu beantworten: Beide Anrufe führen dieselbe Berechnung durch. Möglicher Unterschied des Ergebnisses liegt in der Tatsache, dass getTextBounds Grenzen als Ganzzahl zurückgibt, während measureText gibt den float-Wert zurück.

Was Sie also bekommen, ist ein Rundungsfehler während der Konvertierung von float nach int, und dies geschieht in Paint.cpp in SkPaintGlue :: doTextBounds beim Aufruf der Funktion SkRect :: roundOut.

Die Differenz zwischen der berechneten Breite dieser beiden Aufrufe darf maximal 1 betragen.

EDIT 4 Okt 2011

Was gibt es Schöneres als Visualisierung? Ich habe mir die Mühe gemacht, selbst zu erforschen und mir ein Kopfgeld zu verdienen :)

enter image description here

Dies ist die Schriftgröße 60, in Rot ist ein begrenztes Rechteck, in Lila ist das Ergebnis von measureText.

Es ist zu sehen, dass der linke Teil der Begrenzung 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 zusätzlichen Wert hinzufügt, während getTextBounds minimale Grenzen berechnet, an denen 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);
  }
360
Pointer Null

Meine Erfahrung damit ist, dass getTextBounds das absolute minimale Begrenzungsrechteck zurückgibt, das den Text einkapselt, nicht unbedingt die gemessene Breite, die beim Rendern verwendet wird. Ich möchte auch sagen, dass measureText eine Zeile annimmt.

Um genaue Messergebnisse zu erhalten, sollten Sie den Text mit StaticLayout 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();
21
Chase

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

Die kurze einfache Antwort ist, dass Paint.getTextBounds(String text, int start, int end, Rect bounds)Rect zurückgibt, was nicht bei (0,0) Beginnt. Das heißt, um die tatsächliche Breite des Texts zu erhalten, der durch Aufrufen von Canvas.drawText(String text, float x, float y, Paint paint) mit dem gleichen Paint-Objekt von getTextBounds () festgelegt wird sollte 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 Canvas.drawText() erhalten würden.

Und die gleiche Funktion sollte es sein, height des Textes zu bekommen:

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.


Eine ausführlichere Erklärung finden Sie in this answer.

18
Prizoff

Entschuldigen Sie, dass Sie diese Frage noch einmal beantwortet haben ... Ich musste das Bild einbetten.

Ich denke, die Ergebnisse von @mice sind irreführend. Die Beobachtungen mögen für die Schriftgröße 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.

enter image description here

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 );
    }
  }
12
Moritz

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 tollen Beitrag von Mäusen gelesen hatte, hatte ich einige Probleme, wie man mehrzeiligen Text misst. Es gibt keinen offensichtlichen Weg aus diesen Beiträgen, aber nach einigen Recherchen bin ich durch die StaticLayout-Klasse gegangen. Sie können damit mehrzeiligen Text (Text mit "\ n") messen und über die zugehörige Farbe wesentlich mehr Eigenschaften Ihres Texts 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 Zeilenumbruch 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 für Ihren mehrzeiligen Text erforderliche maximale Breite zu erhalten. 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;
}
10
Moritz

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 deinem 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 als Ihr Ansatz funktioniert.

2
Roman

Der Unterschied zwischen getTextBounds und measureText wird mit dem folgenden Bild beschrieben.

Zusamenfassend,

  1. getTextBounds soll das RECT des exakten Textes erhalten. Das measureText ist die Länge des Texts, einschließlich der zusätzlichen Lücke links und rechts.

  2. Wenn sich zwischen dem Text Leerzeichen befinden, wird er in measureText gemessen, aber nicht in die Länge der TextBounds einbezogen, obwohl die Koordinaten verschoben werden.

  3. Der Text kann nach links gekippt werden. In diesem Fall würde die linke Seite des Begrenzungsrahmens außerhalb der Messung von measureText überschritten, und die Gesamtlänge des gebundenen Texts wäre größer als measureText.

  4. Der Text könnte nach rechts geneigt sein. In diesem Fall würde die rechte Seite des Begrenzungsrahmens außerhalb der Messung von measureText überschreiten, und die Gesamtlänge des gebundenen Texts wäre größer als measureText.

enter image description here

0
Elye

So habe ich die tatsächlichen Abmessungen für den ersten Buchstaben berechnet (Sie können den Methodenkopf an Ihre Bedürfnisse anpassen, d. H. Anstelle von char[] benutze 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.

0
milosmns