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

191

Estou medindo o texto usando Paint.getTextBounds(), pois estou interessado em obter a altura e a largura do texto a serem renderizadas. No entanto, o texto real renderizado é sempre um pouco maior do que o .width()das Rectinformações preenchidas getTextBounds().

Para minha surpresa, testei .measureText()e descobri que ele retorna um valor (mais alto) diferente. Eu tentei e achei correto.

Por que eles relatam larguras diferentes? Como posso obter corretamente a altura e a largura? Quero dizer, eu posso usar .measureText(), mas não saberia se devo confiar nos .height()retornados por getTextBounds().

Conforme solicitado, aqui está o código mínimo para reproduzir o 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())
    );
}

A saída mostra que a diferença não apenas é maior que 1 (e não é um erro de arredondamento de última hora), mas também parece aumentar com o tamanho (eu estava prestes a tirar mais conclusões, mas pode ser totalmente dependente da fonte):

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
fonte
Você pode ver [meu caminho] [1]. Isso pode obter a posição e o limite correto. [1]: stackoverflow.com/a/19979937/1621354
Alex Chi
Explicações relacionadas sobre os métodos de medição de texto para outros visitantes desta pergunta.
Suragch 27/08/19

Respostas:

370

Você pode fazer o que fiz para inspecionar esse problema:

Estude o código-fonte Android, fonte Paint.java, consulte os métodos measureText e getTextBounds. Você aprenderia que o measureText chama native_measureText e getTextBounds chama nativeGetStringBounds, que são métodos nativos implementados em C ++.

Então você continuaria estudando o Paint.cpp, que implementa ambos.

native_measureText -> SkPaintGlue :: measureText_CII

nativeGetStringBounds -> SkPaintGlue :: getStringBounds

Agora, seu estudo verifica onde esses métodos diferem. Após algumas verificações de parâmetros, ambas chamam a função SkPaint :: measureText no Skia Lib (parte do Android), mas ambas chamam a forma sobrecarregada diferente.

Indo mais longe no Skia, vejo que as duas chamadas resultam no mesmo cálculo na mesma função, retornam apenas o resultado de maneira diferente.

Para responder à sua pergunta: As duas chamadas fazem o mesmo cálculo. A possível diferença de resultado reside no fato de que getTextBounds retorna limites como número inteiro, enquanto measureText retorna valor flutuante.

Então, o que você recebe é um erro de arredondamento durante a conversão de float para int, e isso acontece no Paint.cpp em SkPaintGlue :: doTextBounds na chamada para a função SkRect :: roundOut.

A diferença entre a largura calculada dessas duas chamadas pode ser no máximo 1.

EDIT 4 Out 2011

O que pode ser melhor que a visualização. Eu me esforcei, por explorar e por merecer recompensas :)

insira a descrição da imagem aqui

Este é o tamanho da fonte 60, em vermelho é o retângulo de limites , em roxo é o resultado de measureText.

Vimos que a parte esquerda dos limites inicia alguns pixels da esquerda e o valor de measureText é incrementado por esse valor à esquerda e à direita. Isso é chamado de valor AdvanceX da Glyph. (Descobri isso nas fontes Skia em SkPaint.cpp)

Portanto, o resultado do teste é que measureText adiciona algum valor avançado ao texto nos dois lados, enquanto getTextBounds calcula limites mínimos onde o texto fornecido se encaixa.

Espero que este resultado seja útil para você.

Código de teste:

  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);
  }
Ponteiro nulo
fonte
3
Desculpe por demorar tanto para comentar, tive uma semana movimentada. Obrigado por reservar um tempo para cavar o código C ++! Infelizmente, a diferença é maior do que 1, e ocorre mesmo quando se utilizam valores arredondados -. Por exemplo, tamanho de texto configuração de 29.0 resulta na measureText () = 263 e getTextBounds () largura = 260
slezica
Poste um código mínimo com a configuração e medição do Paint, que pode reproduzir o problema.
Ponteiro nulo
Atualizada. Desculpe novamente pelo atraso.
slezica
Estou com Rich aqui. Ótima pesquisa! Recompensa totalmente merecida e premiada. Obrigado pelo seu tempo e esforço!
Slezica 5/10
16
@ ratos Acabei de encontrar esta pergunta novamente (eu sou o OP). Eu tinha esquecido o quão incrível sua resposta foi. Obrigado do futuro! Melhores 100rp que investi!
23312 slezica
21

Minha experiência com isso é que getTextBoundsretornará o retângulo mínimo mínimo absoluto que encapsula o texto, não necessariamente a largura medida usada na renderização. Eu também quero dizer que measureTextassume uma linha.

Para obter resultados de medição precisos, você deve usar o StaticLayoutpara renderizar o texto e extrair as medidas.

Por exemplo:

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();
correr atrás
fonte
Não sabia sobre StaticLayout! getWidth () não funciona, apenas retorna o que eu passo no costructor (que widthnão é maxWidth), mas getLineWith () funcionará. Também encontrei o método estático getDesiredWidth (), embora não haja altura equivalente. Corrija a resposta! Além disso, eu realmente gostaria de saber por que tantos métodos diferentes produzem resultados diferentes.
slezica
Meu mal em relação à largura. Se você deseja que a largura de algum texto seja renderizado em uma única linha, ela measureTextdeve funcionar bem. Caso contrário, se você souber as dimensões da largura (como em onMeasureou onLayout, você pode usar a solução que eu postei acima. Você está tentando redimensionar automaticamente a porcentagem de texto? Além disso, a razão pela qual os limites de texto são sempre um pouco menores é porque exclui qualquer preenchimento de caractere , apenas o menor rect limitada necessária para o desenho.
perseguição
Na verdade, estou tentando redimensionar automaticamente um TextView. Eu também preciso da altura, foi aí que meus problemas começaram! measureWidth () funciona bem caso contrário. Depois disso, fiquei curioso a respeito de porque diferentes abordagens deu valores diferentes
slezica
1
Um aspecto a ser observado também é que uma exibição de texto que envolve e é uma multilinha mede no match_parent a largura e não apenas a largura necessária, o que torna o parâmetro de largura acima para StaticLayout válido. Se você precisar de um redimensionador de texto, confira minha postagem em stackoverflow.com/questions/5033012/…
Chase
Eu precisava ter uma animação de expansão de texto e isso ajudou bastante! Obrigado!
box
18

A resposta dos ratos é ótima ... E aqui está a descrição do problema real:

A resposta curta e simples é que Paint.getTextBounds(String text, int start, int end, Rect bounds)retornos Rectque não começam às (0,0). Ou seja, para obter a largura real do texto que será definido chamando Canvas.drawText(String text, float x, float y, Paint paint)com o mesmo objeto Paint de getTextBounds (), você deve adicionar a posição esquerda de Rect. Algo parecido:

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 isso bounds.left- essa é a chave do problema.

Dessa forma, você receberá a mesma largura de texto que receberia usando Canvas.drawText().

E a mesma função deve ser para obter heighto 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: Eu não testei esse código exato, mas testei a concepção.


Uma explicação muito mais detalhada é dada nesta resposta.

Prizoff
fonte
2
Rect define (b.width) para ser (b.right-b.left) e (b.height) para ser (b.bottom-b.top). Portanto, você pode substituir (b.left + b.width) por (b.right) e (b.bottom + b.height) por (2 * b.bottom-b.top), mas acho que você deseja ter ( b.bottom-b.top), que é o mesmo que (b.height). Eu acho que você está correto sobre a largura (com base na verificação da fonte C ++), getTextWidth () retorna o mesmo valor que (b.right), pode haver alguns erros de arredondamento. (b é uma referência para limites)
Nulano
11

Desculpe por responder novamente a essa pergunta ... Eu precisava incorporar a imagem.

Eu acho que os resultados encontrados pelos ratos são enganosos. As observações podem estar corretas para o tamanho da fonte 60, mas ficam muito mais diferentes quando o texto é menor. Por exemplo. 10 px. Nesse caso, o texto é realmente desenhado além dos limites.

insira a descrição da imagem aqui

Código fonte da captura de tela:

  @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
fonte
Ah, o mistério se arrasta. Ainda estou interessado nisso, mesmo por curiosidade. Deixe-me saber se você descobrir mais alguma coisa, por favor!
slezica
O problema pode ser solucionado um pouco quando você multiplica o tamanho da fonte que deseja medir por um fator digamos 20 e depois divide o resultado por isso. Além disso, você pode adicionar uma largura de 1-2% a mais do tamanho medido para ficar do lado seguro. No final, você terá um texto que é medido em comprimento, mas pelo menos não há texto sobreposto.
Moritz
6
Para desenhar o texto exatamente dentro do retângulo delimitador, você não deve desenhar Texto com X = 0,0f, porque o retângulo delimitador está retornando como ret, não como largura e altura. Para desenhá-lo corretamente, você deve deslocar a coordenada X no valor "-bounds.left", depois será exibida corretamente.
Roman
10

AVISO LEGAL: Esta solução não é 100% precisa em termos de determinação da largura mínima.

Eu também estava tentando descobrir como medir o texto em uma tela. Depois de ler o ótimo post dos ratos, tive alguns problemas sobre como medir texto em várias linhas. Não há uma maneira óbvia dessas contribuições, mas depois de algumas pesquisas, atravesso a classe StaticLayout. Ele permite medir o texto de várias linhas (texto com "\ n") e configurar muito mais propriedades do seu texto através do Paint associado.

Aqui está um trecho que mostra como medir o texto de várias linhas:

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

O wrapwitdh pode determinar se você deseja limitar o texto de múltiplas linhas a uma determinada largura.

Como o StaticLayout.getWidth () retorna apenas esse boundedWidth, você deve executar outra etapa para obter a largura máxima exigida pelo texto de várias linhas. Você pode determinar a largura de cada linha e a largura máxima é a largura da linha mais alta, é claro:

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
fonte
você não deveria estar passando ina getLineWidth (você está só de passagem 0 de cada vez)
Rico S
2

Há outra maneira de medir os limites do texto com precisão. Primeiro, você deve obter o caminho para o Paint e o texto atuais. No seu caso, deve ser assim:

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

Depois disso, você pode ligar para:

mPath.computeBounds(mBoundsPath, true);

No meu código, ele sempre retorna valores corretos e esperados. Mas não tenho certeza se funciona mais rápido que sua abordagem.

romano
fonte
Achei isso útil, já que eu estava usando o getTextPath de qualquer maneira, para desenhar um contorno de traçado ao redor do texto.
Página
1

A diferença entre getTextBoundse measureTexté descrita na imagem abaixo.

Em resumo,

  1. getTextBoundsé obter o RECT do texto exato. O measureTexté o comprimento do texto, incluindo o espaço extra à esquerda e à direita.

  2. Se houver espaços entre o texto, ele será medido, measureTextmas não incluindo, o comprimento dos TextBounds, embora a coordenada seja alterada.

  3. O texto pode estar inclinado (Inclinar) para a esquerda. Nesse caso, o lado esquerdo da caixa delimitadora excederia fora da medida do measureText, e o comprimento geral do texto delimitado seria maior quemeasureText

  4. O texto pode estar inclinado (Inclinar) para a direita. Nesse caso, o lado direito da caixa delimitadora excederia fora da medida do measureText, e o comprimento geral do texto delimitado seria maior quemeasureText

insira a descrição da imagem aqui

Elye
fonte
0

Foi assim que calculei as dimensões reais da primeira letra (você pode alterar o cabeçalho do método para atender às suas necessidades, ou seja, em vez de char[]usar 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;
}

Observe que estou usando TextPaint em vez da classe Paint original.

milosmns
fonte