Android Paint:.measureText() vs getTextBounds()


Estoy midiendo texto usando Paint.getTextBounds(), ya que estoy interesado en obtener tanto la altura como la anchura del texto a representar. Sin embargo, el texto real renderizado es siempre un poco más ancho que el .width() de la información Rect rellenada por getTextBounds().

Para mi sorpresa, probé .measureText(), y descubrí que devuelve un valor diferente (más alto). Lo intenté y lo encontré correcto.

¿Por qué reportan diferentes anchuras? ¿Cómo puedo obtener correctamente la altura y el ancho? Quiero decir, yo puede usar .measureText(), pero entonces no sabría si debería confiar en el .height() devuelto por getTextBounds().

Como se solicitó, aquí hay un código mínimo para reproducir el 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())
    );
}

La salida muestra que la diferencia no solo es mayor que 1 (y no es un error de redondeo de último minuto), sino que también parece aumentar con el tamaño (estaba a punto de sacar más conclusiones, pero puede ser completamente dependiente de la fuente):

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
Author: slezica, 2011-09-26

7 answers

Usted puede hacer lo que yo hice para inspeccionar tal problema:

Estudia el código fuente de Android, Paint.fuente java, vea los métodos measureText y getTextBounds. Aprendería que measureText llama a native_measureText y getTextBounds llama a nativeGetStringBounds, que son métodos nativos implementados en C++.

Así que continuarías estudiando Pintura.cpp, que implementa ambos.

Native_measureText - > SkPaintGlue:: measureText_CII

NativeGetStringBounds - > SkPaintGlue:: getStringBounds

Ahora su estudio comprueba dónde difieren estos métodos. Después de algunas comprobaciones param, ambos llaman a la función SKPaint:: measureText en Skia Lib (parte de Android), pero ambos llaman a diferentes formularios sobrecargados.

Profundizando en Skia, veo que ambas llamadas resultan en el mismo cálculo en la misma función, solo devuelven el resultado de manera diferente.

Para responder A su pregunta: Ambas llamadas hacen el mismo cálculo. Posible diferencia de resultado se encuentra de hecho que getTextBoundsdevuelve los límites como enteros, mientras que measureText devuelve un valor flotante.

Así que lo que obtienes es un error de redondeo durante la conversión de float a int, y esto sucede en Paint.cpp en SkPaintGlue::doTextBounds en llamada a la función SkRect:: roundOut.

La diferencia entre el ancho calculado de esas dos llamadas puede ser máxima 1.

EDITAR 4 Oct 2011

Lo que puede ser mejor que la visualización. Tomé el esfuerzo, para el propio explorar, y para merecer recompensa:)

introduzca la descripción de la imagen aquí

Este es el tamaño de fuente 60, en rojo es límites rectángulo, en púrpura es el resultado de measureText.

Se ve que la parte izquierda de los límites comienza algunos píxeles desde la izquierda, y el valor de measureText se incrementa por este valor tanto a la izquierda como a la derecha. Esto es algo llamado valor AdvanceX de Glifo. (He descubierto esto en Skia fuentes en SKPaint.cpp)

Así que el resultado de la prueba es que measureText agrega algún avance valor para el texto en ambos lados, mientras que getTextBounds calcula límites mínimos donde se ajustará el texto dado.

Espero que este resultado sea útil para usted.

Código de prueba:

  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);
  }
 338
Author: Pointer Null,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-11 22:10:37

Mi experiencia con esto es que getTextBounds devolverá ese recto de límite mínimo absoluto que encapsula el texto, no necesariamente el ancho medido utilizado al renderizar. También quiero decir que measureText asume una línea.

Para obtener resultados de medición precisos, debe usar StaticLayout para renderizar el texto y extraer las mediciones.

Por ejemplo:

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();
 19
Author: Chase,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-25 00:03:45

La respuesta de los ratones es genial... Y aquí está la descripción del verdadero problema:

La respuesta corta y simple es que Paint.getTextBounds(String text, int start, int end, Rect bounds) devuelve Rect que no comienza en (0,0). Es decir, para obtener el ancho real del texto que se establecerá llamando a Canvas.drawText(String text, float x, float y, Paint paint) con el mismo objeto Paint de getTextBounds() debe agregar la posición izquierda de Rect. Algo así:

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

Observe esto bounds.left - esta es la clave del problema.

De esta manera usted recibirá la misma anchura de texto, que recibirías usando Canvas.drawText().

Y la misma función debería ser para obtener height del texto:

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

P. S.: No probé este código exacto, pero probé la concepción.


Se da una explicación mucho más detallada en esta respuesta.

 16
Author: Prizoff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:55:05

Perdón por responder de nuevo a esa pregunta... Necesitaba incrustar la imagen.

Creo que los resultados que @mice encontró son erróneos. Las observaciones pueden ser correctas para el tamaño de fuente de 60, pero se vuelven mucho más diferentes cuando el texto es más pequeño. Eg. 10px. En ese caso, el texto se dibuja realmente MÁS ALLÁ de los límites.

introduzca la descripción de la imagen aquí

Código fuente de la captura de pantalla:

  @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
Author: Moritz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-01 15:43:32

DESCARGO DE RESPONSABILIDAD: Esta solución no es 100% precisa en términos de determinar el ancho mínimo.

También estaba averiguando cómo medir el texto en un lienzo. Después de leer el gran post de ratones tuve algunos problemas sobre cómo medir texto multilínea. No hay una manera obvia de estas contribuciones, pero después de algunas investigaciones cam a través de la clase StaticLayout. Le permite medir texto multilínea (texto con "\n") y configurar muchas más propiedades de su texto a través de la pintura asociada.

Aquí hay un fragmento que muestra cómo medir texto multilínea:

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

El wrapwitdh es capaz de determinar si desea limitar su texto multilínea a un cierto ancho.

Desde el StaticLayout.getWidth () solo devuelve este boundedWidth tienes que dar otro paso para obtener el ancho máximo requerido por tu texto multilínea. Usted es capaz de determinar el ancho de cada línea y el ancho máximo es el ancho de línea más alto por supuesto:

private float getMaxLineWidth( StaticLayout layout ) {
    float maxLine = 0.0f;
    int lineCount = layout.getLineCount();
    for( int i = 0; i < lineCount; i++ ) {
        if( layout.getLineWidth( 0 ) > maxLine ) {
            maxLine = layout.getLineWidth( 0 );
        }
    }
    return maxLine;
}
 9
Author: Moritz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-15 08:16:03

Hay otra forma de medir los límites del texto con precisión, primero debe obtener la ruta para la pintura y el texto actuales. En tu caso debería ser así:

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

Después de eso puedes llamar:

mPath.computeBounds(mBoundsPath, true);

En mi código siempre devuelve valores correctos y esperados. Pero, no estoy seguro de si funciona más rápido que su enfoque.

 1
Author: Roman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-12 09:05:48

Así es como calculé las dimensiones reales para la primera letra (puede cambiar el encabezado del método para adaptarse a sus necesidades, es decir, en lugar de 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;
}

Tenga en cuenta que estoy usando TextPaint en lugar de la clase Paint original.

 0
Author: milosmns,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-03 08:38:33