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


191

Sto misurando il testo usando Paint.getTextBounds(), dal momento che mi interessa ottenere sia l'altezza che la larghezza del testo da rendere. Tuttavia, il testo reale reso è sempre un po 'più ampio di quello .width()delle Rectinformazioni riempite da getTextBounds().

Con mia sorpresa, ho testato .measureText()e ho scoperto che restituisce un valore diverso (superiore). Ci ho provato e l'ho trovato corretto.

Perché segnalano larghezze diverse? Come posso ottenere correttamente l'altezza e la larghezza? Voglio dire, posso usare .measureText(), ma poi non saprei se dovrei fidarmi del .height()reso getTextBounds().

Come richiesto, ecco un codice minimo per riprodurre il problema:

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

L'output mostra che la differenza non solo diventa maggiore di 1 (e non è un errore di arrotondamento dell'ultimo minuto), ma sembra anche aumentare con le dimensioni (stavo per trarre più conclusioni, ma potrebbe essere interamente dipendente dal carattere):

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

Puoi vedere [a modo mio] [1]. Ciò può ottenere la posizione e il limite corretto. [1]: stackoverflow.com/a/19979937/1621354
Alex Chi,

Risposte:


370

Puoi fare quello che ho fatto per ispezionare questo problema:

Studia il codice sorgente Android, il codice Paint.java, vedi entrambi i metodi measureText e getTextBounds. Impareresti che measureText chiama native_measureText e getTextBounds chiama nativeGetStringBounds, che sono metodi nativi implementati in C ++.

Quindi continueresti a studiare Paint.cpp, che implementa entrambi.

native_measureText -> SkPaintGlue :: measureText_CII

nativeGetStringBounds -> SkPaintGlue :: getStringBounds

Ora il tuo studio controlla dove questi metodi differiscono. Dopo alcuni controlli dei parametri, entrambi chiamano la funzione SkPaint :: measureText in Skia Lib (parte di Android), ma entrambi chiamano diversi moduli sovraccarichi.

Scavando ulteriormente in Skia, vedo che entrambe le chiamate risultano nello stesso calcolo nella stessa funzione, restituiscono il risultato solo in modo diverso.

Per rispondere alla tua domanda: entrambe le chiamate eseguono lo stesso calcolo. La possibile differenza di risultato sta infatti nel fatto che getTextBounds restituisce i limiti come numero intero, mentre measureText restituisce un valore float.

Quindi quello che ottieni è un errore di arrotondamento durante la conversione di float in int, e questo accade in Paint.cpp in SkPaintGlue :: doTextBounds in chiamata per la funzione SkRect :: roundOut.

La differenza tra la larghezza calcolata di queste due chiamate può essere al massimo 1.

EDIT 4 ott 2011

Cosa può esserci di meglio della visualizzazione. Ho fatto lo sforzo, per esplorare me stesso e per meritare la grazia :)

inserisci qui la descrizione dell'immagine

Questa è la dimensione del carattere 60, in rosso è il rettangolo dei limiti , in viola è il risultato di measureText.

Si vede che la parte sinistra dei limiti inizia alcuni pixel da sinistra e il valore di measureText viene incrementato di questo valore sia a sinistra che a destra. Questo è qualcosa chiamato valore AdvanceX di Glyph. (L'ho scoperto nelle fonti Skia in SkPaint.cpp)

Quindi il risultato del test è che measureText aggiunge un valore avanzato al testo su entrambi i lati, mentre getTextBounds calcola i limiti minimi in cui il testo specificato si adatterà.

Spero che questo risultato ti sia utile.

Codice di prova:

  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
Scusate il tempo di commentare così tanto, ho avuto una settimana intensa. Grazie per aver dedicato del tempo a scavare nel codice C ++! Sfortunatamente, la differenza è maggiore di 1 e si verifica anche quando si utilizzano valori arrotondati, ad esempio impostando la dimensione del testo su 29,0 si ottengono in measureText () = 263 e getTextBounds ().
Width

Si prega di inserire un codice minimo con l'installazione e la misurazione di Paint, in grado di riprodurre il problema.
Puntatore Null

Aggiornato. Scusami ancora per il ritardo.
slezica,

Sono con Rich qui. Ottima ricerca! Bounty meritato e premiato. Grazie per il tuo tempo e sforzo!
slezica,

16
@mice Ho appena trovato di nuovo questa domanda (sono l'OP). Avevo dimenticato quanto incredibile fosse la tua risposta. Grazie dal futuro! I migliori 100rp che ho investito!
slezica,

21

La mia esperienza con questo è che getTextBoundsrestituirà quel rettangolo di limite minimo assoluto che incapsula il testo, non necessariamente la larghezza misurata utilizzata durante il rendering. Voglio anche dire che measureTextassume una riga.

Per ottenere risultati di misurazione precisi, è necessario utilizzare il StaticLayout per rendere il testo ed estrarre le misurazioni.

Per esempio:

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

Non sapevo di StaticLayout! getWidth () non funzionerà, restituisce solo ciò che passo nel costruttore (che è width, non maxWidth), ma getLineWith () funzionerà. Ho anche trovato il metodo statico getDesiredWidth (), anche se non esiste un equivalente in altezza. Per favore, correggi la risposta! Inoltre, vorrei davvero sapere perché così tanti metodi diversi producono risultati diversi.
slezica,

Il mio male per quanto riguarda la larghezza. Se si desidera la larghezza di un testo che verrà visualizzato su una sola riga, measureTextdovrebbe funzionare correttamente. Altrimenti, se conosci la larghezza mantenuta (come in onMeasureo onLayout, puoi usare la soluzione che ho pubblicato sopra. Stai cercando di ridimensionare automaticamente il testo per caso? Inoltre, il motivo per cui i limiti del testo è sempre leggermente più piccolo è perché esclude qualsiasi riempimento di caratteri , solo il più piccolo retto limitato necessario per disegnare.
Insegui il

Sto davvero cercando di ridimensionare automaticamente un TextView. Ho bisogno anche dell'altezza, ecco dove sono iniziati i miei problemi! measureWidth () funziona bene altrimenti. Dopodiché, mi sono incuriosito il motivo per cui approcci diversi hanno dato valori diversi
slezica il

1
Una cosa da notare anche è che una visualizzazione di testo che avvolge ed è una multilinea misurerà in match_parent per la larghezza e non solo la larghezza richiesta, il che rende valido il parametro di larghezza sopra a StaticLayout. Se hai bisogno di un ridimensionatore di testo, dai un'occhiata al mio post su stackoverflow.com/questions/5033012/…
Insegui il

Avevo bisogno di avere un'animazione di espansione del testo e questo mi ha aiutato un sacco! Grazie!
scatola

18

La risposta dei topi è fantastica ... Ed ecco la descrizione del vero problema:

La breve risposta semplice è che Paint.getTextBounds(String text, int start, int end, Rect bounds)ritorna Rectche non inizia alle (0,0). Cioè, per ottenere l'effettiva larghezza del testo che verrà impostata chiamando Canvas.drawText(String text, float x, float y, Paint paint)con lo stesso oggetto Paint da getTextBounds () è necessario aggiungere la posizione sinistra di Rect. Qualcosa del genere:

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

Nota questo bounds.left: questa è la chiave del problema.

In questo modo riceverai la stessa larghezza di testo che riceveresti usando Canvas.drawText().

E la stessa funzione dovrebbe essere per ottenere heightil testo:

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: Non ho testato questo codice esatto, ma ho testato il concepimento.


In questa risposta viene fornita una spiegazione molto più dettagliata .


2
Rect definisce (b.width) come (b.right-b.left) e (b.height) come (b.bottom-b.top). Pertanto è possibile sostituire (b.left + b.width) con (b.right) e (b.bottom + b.height) con (2 * b.bottom-b.top), ma penso che tu voglia avere ( b.bottom-b.top) invece, che è lo stesso di (b.height). Penso che tu abbia ragione sulla larghezza (basata sul controllo del sorgente C ++), getTextWidth () restituisce lo stesso valore di (b.right), potrebbero esserci degli errori di arrotondamento. (b è un riferimento ai limiti)
Nulano,

11

Ci scusiamo per aver risposto ancora a questa domanda ... Avevo bisogno di incorporare l'immagine.

Penso che i risultati trovati da @mice siano fuorvianti. Le osservazioni potrebbero essere corrette per la dimensione del carattere di 60 ma diventano molto più diverse quando il testo è più piccolo. Per esempio. 10px. In tal caso, il testo viene effettivamente disegnato OLTRE i limiti.

inserisci qui la descrizione dell'immagine

Codice sorgente dello screenshot:

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

Ah, il mistero si trascina. Sono ancora interessato a questo, anche se per curiosità. Fammi sapere se scopri qualcos'altro, per favore!
slezica,

Il problema può essere risolto un po 'quando moltiplichi la dimensione del carattere che vuoi misurare per un fattore diciamo 20 e poi dividi il risultato per quello. DIVENTA si potrebbe anche voler aggiungere 1-2% in più di larghezza della dimensione misurata solo per essere al sicuro. Alla fine avrai un testo misurato come lungo ma almeno non c'è sovrapposizione di testo.
Moritz,

6
Per disegnare il testo esattamente all'interno del rettangolo di delimitazione non devi disegnare Testo con X = 0.0f, perché il rettangolo di rettifica sta tornando come rettangolo, non come larghezza e altezza. Per disegnarlo correttamente dovresti sfalsare la coordinata X sul valore "-bounds.left", quindi verrà mostrata correttamente.
Romano,

10

NOTA BENE: questa soluzione non è precisa al 100% in termini di determinazione della larghezza minima.

Stavo anche scoprendo come misurare il testo su una tela. Dopo aver letto l'ottimo post dei topi ho avuto dei problemi su come misurare il testo multilinea. Non vi è alcun modo ovvio da questi contributi, ma dopo alcune ricerche sono passato attraverso la classe StaticLayout. Ti permette di misurare il testo multilinea (testo con "\ n") e configurare molte più proprietà del tuo testo tramite il Paint associato.

Ecco uno snippet che mostra come misurare il testo multilinea:

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

Il wrapwitdh è in grado di determinare se si desidera limitare il testo multilinea a una determinata larghezza.

Poiché StaticLayout.getWidth () restituisce solo boundedWidth, è necessario fare un altro passo per ottenere la larghezza massima richiesta dal testo multilinea. Puoi determinare la larghezza di ogni linea e la larghezza massima è ovviamente la larghezza della linea più alta:

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

non dovresti passare ia getLineWidth (ne passi solo 0 ogni volta)
Rich S

2

C'è un altro modo per misurare con precisione i limiti del testo, prima dovresti ottenere il percorso per il Paint e il testo correnti. Nel tuo caso dovrebbe essere così:

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

Dopodiché puoi chiamare:

mPath.computeBounds(mBoundsPath, true);

Nel mio codice restituisce sempre valori corretti e previsti. Ma non sono sicuro che funzioni più velocemente del tuo approccio.


L'ho trovato utile da quando stavo facendo getTextPath comunque, al fine di tracciare un contorno del tratto attorno al testo.
ToolmakerSteve

2

La differenza tra getTextBoundse measureTextè descritta con l'immagine qui sotto.

In breve,

  1. getTextBoundsè ottenere il RECT del testo esatto. La measureTextè la lunghezza del testo, compreso il divario supplementare sulla destra e la sinistra.

  2. Se ci sono spazi tra il testo, questo viene misurato measureTextma non incluso nella lunghezza dei TextBounds, sebbene le coordinate vengano spostate.

  3. Il testo potrebbe essere inclinato (inclinato) a sinistra. In questo caso, il lato sinistro del riquadro di delimitazione supererebbe al di fuori della misura del testo della misura e la lunghezza complessiva del limite del testo sarebbe maggiore dimeasureText

  4. Il testo potrebbe essere inclinato (inclinato) a destra. In questo caso, il lato destro del riquadro di delimitazione supererebbe al di fuori della misura del testo della misura e la lunghezza complessiva del limite del testo sarebbe maggiore dimeasureText

inserisci qui la descrizione dell'immagine


0

Ecco come ho calcolato le dimensioni reali per la prima lettera (è possibile modificare l'intestazione del metodo in base alle proprie esigenze, ovvero anziché char[]utilizzare 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;
}

Nota che sto usando TextPaint invece della classe Paint originale.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.