Pintura de Android: .measureText () vs .getTextBounds ()

191

Estoy midiendo texto usando Paint.getTextBounds(), ya que estoy interesado en obtener tanto el alto como el ancho del texto que se representará. Sin embargo, el texto real representado siempre es un poco más ancho que el .width()de la Rectinformación completada getTextBounds().

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

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

Según lo solicitado, 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())
    );
}

El resultado 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
slezica
fuente
Puedes ver [mi camino] [1]. Eso puede obtener la posición y el límite correcto. [1]: stackoverflow.com/a/19979937/1621354
Alex Chi
Explicación relacionada sobre los métodos de medición de texto para otros visitantes a esta pregunta.
Suragch

Respuestas:

370

Puedes hacer lo que hice para inspeccionar tal problema:

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

Entonces continuaría estudiando Paint.cpp, que implementa ambos.

native_measureText -> SkPaintGlue :: measureText_CII

nativeGetStringBounds -> SkPaintGlue :: getStringBounds

Ahora su estudio verifica dónde difieren estos métodos. Después de algunas comprobaciones de parámetros, ambos llaman a la función SkPaint :: measureText en Skia Lib (parte de Android), pero ambos llaman a diferentes formas sobrecargadas.

Al profundizar más 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. La posible diferencia de resultado radica en que getTextBounds devuelve los límites como un entero, mientras que measureText devuelve el valor flotante.

Entonces, 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 la 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

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

ingrese la descripción de la imagen aquí

Este es el tamaño de fuente 60, en rojo es rectángulo de límites , 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 en este valor tanto a la izquierda como a la derecha. Esto es algo llamado valor AdvanceX de Glyph. (He descubierto esto en las fuentes de Skia en SkPaint.cpp)

Por lo tanto, el resultado de la prueba es que measureText agrega un valor de avance al texto en ambos lados, mientras que getTextBounds calcula los límites mínimos donde se ajustará el texto dado.

Espero que este resultado te sea útil.

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);
  }
Puntero nulo
fuente
3
Perdón por tomarme tanto tiempo para comentar, tuve una semana ocupada. ¡Gracias por tomarse el tiempo de profundizar en el código C ++! Desafortunadamente, la diferencia es mayor que 1, y ocurre incluso cuando se usan valores redondeados, por ejemplo, al establecer el tamaño del texto en 29.0 resulta en measureText () = 263 y getTextBounds (). Width = 260
slezica
Publique un código mínimo con la configuración y medición de Paint, que puede reproducir el problema.
Puntero nulo
Actualizado. Perdón por el retraso.
slezica
Estoy con Rich aquí. ¡Gran investigación! Recompensa totalmente merecida y premiada. ¡Gracias por tu tiempo y esfuerzo!
slezica
16
@ ratones Acabo de encontrar esta pregunta nuevamente (soy el OP). Había olvidado lo increíble que era tu respuesta. Gracias desde el futuro! ¡El mejor 100rp que he invertido!
slezica
21

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

Para obtener resultados de medición precisos, debe utilizar el StaticLayout para representar 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();
Persecución
fuente
¡No sabía sobre StaticLayout! getWidth () no funcionará, solo devuelve lo que paso en el costructor (que es width, no maxWidth), pero getLineWith () lo hará. También encontré el método estático getDesiredWidth (), aunque no hay una altura equivalente. Por favor corrija la respuesta! Además, me gustaría saber por qué tantos métodos diferentes producen resultados diferentes.
slezica
Mi mal con respecto al ancho. Si desea el ancho de un texto que se representará en una sola línea, measureTextdebería funcionar bien. De lo contrario, si conoce las constantes de ancho (como en onMeasureo onLayout, puede usar la solución que publiqué anteriormente. ¿Está tratando de redimensionar automáticamente la posibilidad de texto? Además, la razón por la cual los límites del texto siempre son ligeramente más pequeños es porque excluye cualquier relleno de caracteres , el rectángulo acotado más pequeño necesario para dibujar
Chase
De hecho, estoy tratando de redimensionar automáticamente un TextView. También necesito la altura, ¡ahí es donde comenzaron mis problemas! measureWidth () funciona bien de lo contrario. Después de eso, tuve curiosidad por saber por qué diferentes enfoques daban valores diferentes
slezica
1
Una cosa a tener en cuenta también es que una vista de texto que se ajusta y es multilínea medirá en match_parent para el ancho y no solo el ancho requerido, lo que hace que el parámetro de ancho anterior a StaticLayout sea válido. Si necesita un cambio de tamaño de texto, consulte mi publicación en stackoverflow.com/questions/5033012/…
Chase
¡Necesitaba tener una animación de expansión de texto y esto ayudó mucho! ¡Gracias!
caja
18

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

La respuesta breve y sencilla es que Paint.getTextBounds(String text, int start, int end, Rect bounds)vuelve Rect, que no comienza a (0,0). Es decir, para obtener el ancho real del texto que se establecerá llamando 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 como eso:

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 forma, recibirá el mismo ancho de texto que recibiría con Canvas.drawText().

Y la misma función debería ser para obtener heightel 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;
}

Ps: no probé este código exacto, pero probé la concepción.


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

Prizoff
fuente
2
Rect define (b.width) como (b.right-b.left) y (b.height) como (b.bottom-b.top). Por lo tanto, puede reemplazar (b.left + b.width) con (b.right) y (b.bottom + b.height) con (2 * b.bottom-b.top), pero creo que desea tener ( b.bottom-b.top), que es lo mismo que (b.height). Creo que tiene razón sobre el ancho (basado en la comprobación de la fuente C ++), getTextWidth () devuelve el mismo valor que (b.right), puede haber algunos errores de redondeo. (b es una referencia a los límites)
Nulano
11

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

Creo que los resultados que @mice encontró son engañosos. 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. P.ej. 10px. En ese caso, el texto se dibuja MÁS ALLÁ de los límites.

ingrese 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 );
    }
  }
Moritz
fuente
Ah, el misterio se prolonga. Todavía estoy interesado en esto, incluso por curiosidad. ¡Avísame si descubres algo más, por favor!
slezica
El problema se puede solucionar un poco cuando multiplica el tamaño de letra que desea medir por un factor de digamos 20 y luego divide el resultado por eso. ENTONCES, es posible que también desee agregar un 1-2% más de ancho del tamaño medido solo para estar seguro. Al final, tendrá un texto que se mide como largo pero al menos no hay superposición de texto.
Moritz
66
Para dibujar el texto exactamente dentro del límite delimitador, no debe dibujar Texto con X = 0.0f, porque el límite delimitador regresa como rect, no como ancho y alto. Para dibujarlo correctamente, debe compensar la coordenada X en el valor "-bounds.left", luego se mostrará correctamente.
Romano
10

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

También estaba descubriendo cómo medir texto en un lienzo. Después de leer la gran publicación 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, he podido ver 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 Paint asociado.

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 puede determinar si desea limitar su texto multilínea a un cierto ancho.

Dado que StaticLayout.getWidth () solo devuelve este boundedWidth, debe dar otro paso para obtener el ancho máximo requerido por su texto multilínea. Puede 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( i ) > maxLine ) {
            maxLine = layout.getLineWidth( i );
        }
    }
    return maxLine;
}
Moritz
fuente
no deberías pasar ial getLineWidth (solo estás pasando 0 cada vez)
Rich S
2

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 su 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 los valores correctos y esperados. Pero, no estoy seguro si funciona más rápido que su enfoque.

romano
fuente
Encontré esto útil ya que estaba haciendo getTextPath de todos modos, para dibujar un contorno de trazo alrededor del texto.
ToolmakerSteve
1

La diferencia entre getTextBoundsy measureTextse describe con la imagen a continuación.

En breve,

  1. getTextBoundses obtener el RECT del texto exacto. El measureTextes la longitud del texto, incluido el espacio adicional a la izquierda y a la derecha.

  2. Si hay espacios entre el texto, se mide measureTextpero no se incluye en la longitud de los TextBounds, aunque las coordenadas se desplazan.

  3. El texto podría inclinarse (sesgarse) a la izquierda. En este caso, el lado izquierdo del cuadro delimitador excedería fuera de la medición del measureText, y la longitud total del texto encuadernado sería mayor quemeasureText

  4. El texto podría inclinarse (sesgarse) a la derecha. En este caso, el lado derecho del cuadro delimitador excedería fuera de la medición de measureText, y la longitud total del texto encuadernado sería mayor quemeasureText

ingrese la descripción de la imagen aquí

Elye
fuente
0

Así es como calculé las dimensiones reales para la primera letra (puede cambiar el encabezado del método para satisfacer sus necesidades, es decir, en lugar de char[]usarlo 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.

milosmns
fuente