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

191

Je mesure du texte en utilisant Paint.getTextBounds(), car je suis intéressé à obtenir à la fois la hauteur et la largeur du texte à rendre. Cependant, le texte réel rendu est toujours un peu plus large que .width()les Rectinformations renseignées par getTextBounds().

À ma grande surprise, j'ai testé .measureText()et constaté qu'il renvoie une valeur différente (supérieure). Je l'ai essayé et je l'ai trouvé correct.

Pourquoi signalent-ils des largeurs différentes? Comment puis-je obtenir correctement la hauteur et la largeur? Je veux dire, je peux utiliser .measureText(), mais je ne saurais pas si je devrais faire confiance au .height()retour getTextBounds().

Comme demandé, voici un code minimal pour reproduire le problème:

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 sortie montre que la différence est non seulement supérieure à 1 (et qu'il ne s'agit pas d'une erreur d'arrondi de dernière minute), mais semble également augmenter avec la taille (j'étais sur le point de tirer plus de conclusions, mais cela peut être entièrement dépendant de la police):

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
la source
Vous pouvez voir [ma façon] [1]. Cela peut obtenir la position et corriger la borne. [1]: stackoverflow.com/a/19979937/1621354
Alex Chi
Explication connexe sur les méthodes de mesure du texte pour les autres visiteurs de cette question.
Suragch

Réponses:

370

Vous pouvez faire ce que j'ai fait pour inspecter ce problème:

Étudiez le code source Android, Paint.java source, consultez les méthodes measureText et getTextBounds. Vous apprendrez que measureText appelle native_measureText et getTextBounds appelle nativeGetStringBounds, qui sont des méthodes natives implémentées en C ++.

Vous continuerez donc à étudier Paint.cpp, qui implémente les deux.

native_measureText -> SkPaintGlue :: measureText_CII

nativeGetStringBounds -> SkPaintGlue :: getStringBounds

Maintenant, votre étude vérifie où ces méthodes diffèrent. Après quelques vérifications de paramètres, les deux appellent la fonction SkPaint :: measureText dans Skia Lib (partie d'Android), mais ils appellent tous deux une forme surchargée différente.

En creusant plus loin dans Skia, je vois que les deux appels aboutissent au même calcul dans la même fonction, ne retournent que le résultat différemment.

Pour répondre à votre question: vos deux appels font le même calcul. La différence possible de résultat réside en fait que getTextBounds renvoie les limites sous forme d'entier, tandis que measureText renvoie une valeur flottante.

Donc, ce que vous obtenez est une erreur d'arrondi lors de la conversion de float en int, et cela se produit dans Paint.cpp dans SkPaintGlue :: doTextBounds lors de l'appel à la fonction SkRect :: roundOut.

La différence entre la largeur calculée de ces deux appels peut être au maximum de 1.

EDIT 4 octobre 2011

Quoi de mieux que la visualisation. J'ai pris l'effort, pour ma propre exploration et pour mériter une prime :)

entrez la description de l'image ici

Il s'agit de la taille de police 60, en rouge le rectangle des limites , en violet le résultat de measureText.

On voit que les limites de la partie gauche commencent quelques pixels à partir de la gauche, et la valeur de measureText est incrémentée de cette valeur à gauche et à droite. C'est quelque chose appelé la valeur AdvanceX de Glyph. (J'ai découvert cela dans les sources Skia dans SkPaint.cpp)

Ainsi, le résultat du test est que measureText ajoute une valeur avancée au texte des deux côtés, tandis que getTextBounds calcule les limites minimales où le texte donné s'adaptera.

J'espère que ce résultat vous sera utile.

Code de test:

  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);
  }
Pointeur nul
la source
3
Désolé d'avoir mis si longtemps à commenter, j'ai eu une semaine chargée. Merci d'avoir pris le temps de fouiller dans le code C ++! Malheureusement, la différence est supérieure à 1, et cela se produit même lorsque vous utilisez des valeurs arrondies - par exemple, la définition de la taille du texte à 29,0 donne mesureText () = 263 et getTextBounds (). Width = 260
slezica
Veuillez publier un code minimal avec la configuration et la mesure de Paint, qui peut reproduire le problème.
Pointer Null
Actualisé. Désolé encore pour le retard.
slezica
Je suis avec Rich ici. Grande recherche! Bounty pleinement mérité et récompensé. Merci pour votre temps et vos efforts!
slezica
16
@mice Je viens de retrouver cette question (je suis l'OP). J'avais oublié à quel point votre réponse était incroyable. Merci du futur! Meilleur 100rp que j'ai investi!
slezica
21

Mon expérience avec ceci est que getTextBoundsretournera ce rectangle de délimitation minimal absolu qui encapsule le texte, pas nécessairement la largeur mesurée utilisée lors du rendu. Je veux également dire que cela measureTextsuppose une ligne.

Afin d'obtenir des résultats de mesure précis, vous devez utiliser le StaticLayoutpour rendre le texte et extraire les mesures.

Par exemple:

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();
Chasse
la source
Je ne connaissais pas StaticLayout! getWidth () ne fonctionnera pas, il renvoie simplement ce que je passe dans le costructor (ce qui n'est widthpas le cas maxWidth), mais getLineWith () le fera. J'ai également trouvé la méthode statique getDesiredWidth (), bien qu'il n'y ait pas d'équivalent en hauteur. Veuillez corriger la réponse! Aussi, j'aimerais vraiment savoir pourquoi tant de méthodes différentes donnent des résultats différents.
slezica
Mon mauvais en ce qui concerne la largeur. Si vous voulez la largeur d'un texte qui sera rendu sur une seule ligne, cela measureTextdevrait fonctionner correctement. Sinon, si vous connaissez les contraintes de largeur (comme dans onMeasureou onLayout, vous pouvez utiliser la solution que j'ai publiée ci-dessus. Essayez-vous de redimensionner automatiquement le texte par hasard? De plus, la raison pour laquelle les limites du texte sont toujours légèrement plus petites est qu'elle exclut tout remplissage de caractères , juste le plus petit rect borné nécessaire pour dessiner.
Poursuite du
J'essaye en effet de redimensionner automatiquement un TextView. J'ai aussi besoin de la hauteur, c'est là que mes problèmes ont commencé! measureWidth () fonctionne bien sinon. Après cela, je suis devenu curieux de savoir pourquoi différentes approches donnaient des valeurs différentes
slezica
1
Une chose à noter également est qu'une vue de texte qui encapsule et est une multiligne mesurera à match_parent pour la largeur et pas seulement la largeur requise, ce qui rend le paramètre de largeur ci-dessus valide à StaticLayout. Si vous avez besoin d'un redimensionneur de texte, consultez mon message sur stackoverflow.com/questions/5033012/...
Chase
J'avais besoin d'une animation d'expansion de texte et cela a aidé beaucoup! Je vous remercie!
boîte
18

La réponse des souris est excellente ... Et voici la description du vrai problème:

La réponse courte et simple est que les Paint.getTextBounds(String text, int start, int end, Rect bounds)retours Rectne commencent pas à (0,0). Autrement dit, pour obtenir la largeur réelle du texte qui sera définie en appelant Canvas.drawText(String text, float x, float y, Paint paint)avec le même objet Paint depuis getTextBounds (), vous devez ajouter la position gauche de Rect. Quelque chose comme ca:

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

Remarquez ceci bounds.left- c'est la clé du problème.

De cette façon, vous recevrez la même largeur de texte que celle que vous recevriez en utilisant Canvas.drawText().

Et la même fonction devrait être pour obtenir heightdu texte:

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: Je n'ai pas testé ce code exact, mais testé la conception.


Une explication beaucoup plus détaillée est donnée dans cette réponse.

Prizoff
la source
2
Rect définit (b.width) comme étant (b.right-b.left) et (b.height) comme (b.bottom-b.top). Par conséquent, vous pouvez remplacer (b.left + b.width) par (b.right) et (b.bottom + b.height) par (2 * b.bottom-b.top), mais je pense que vous voulez avoir ( b.bottom-b.top) à la place, qui est identique à (b.height). Je pense que vous avez raison sur la largeur (basée sur la vérification de la source C ++), getTextWidth () renvoie la même valeur que (b.right), il peut y avoir des erreurs d'arrondi. (b est une référence aux limites)
Nulano
11

Désolé d'avoir répondu à nouveau à cette question ... J'avais besoin d'intégrer l'image.

Je pense que les résultats trouvés par @mice sont trompeurs. Les observations peuvent être correctes pour la taille de police de 60, mais elles deviennent beaucoup plus différentes lorsque le texte est plus petit. Par exemple. 10px. Dans ce cas, le texte est en fait dessiné AU-DELÀ des limites.

entrez la description de l'image ici

Code source de la capture d'écran:

  @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
la source
Ah, le mystère s'éternise. Cela m'intéresse toujours, même par curiosité. Faites-moi savoir si vous découvrez autre chose, s'il vous plaît!
slezica
Le problème peut être un peu résolu lorsque vous multipliez la taille de police que vous souhaitez mesurer par un facteur de disons 20 et que vous divisez le résultat par cela. QUE vous voudrez peut-être également ajouter 1 à 2% de largeur supplémentaire à la taille mesurée juste pour être du bon côté. À la fin, vous aurez un texte qui est mesuré comme long mais au moins il n'y a pas de chevauchement de texte.
Moritz le
6
Pour dessiner le texte exactement à l'intérieur du rect englobant, vous ne devez pas dessiner du texte avec X = 0.0f, car le rect englobant revient comme rect, pas comme la largeur et la hauteur. Pour le dessiner correctement, vous devez décaler la coordonnée X sur la valeur "-bounds.left", puis elle sera affichée correctement.
Roman
10

AVERTISSEMENT: Cette solution n'est pas précise à 100% en termes de détermination de la largeur minimale.

Je cherchais également à mesurer du texte sur une toile. Après avoir lu le super article de souris, j'ai eu quelques problèmes sur la façon de mesurer du texte multiligne. Il n'y a pas de moyen évident de ces contributions, mais après quelques recherches, je suis tombé sur la classe StaticLayout. Il vous permet de mesurer du texte multiligne (texte avec "\ n") et de configurer beaucoup plus de propriétés de votre texte via le Paint associé.

Voici un extrait de code montrant comment mesurer du texte multiligne:

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

Le wrapwitdh est capable de déterminer si vous souhaitez limiter votre texte multiligne à une certaine largeur.

Puisque StaticLayout.getWidth () ne renvoie que ce boundedWidth, vous devez faire un autre pas pour obtenir la largeur maximale requise par votre texte multiligne. Vous pouvez déterminer la largeur de chaque ligne et la largeur maximale est bien sûr la largeur de ligne la plus élevée:

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
la source
ne devriez-vous pas passer idans le getLineWidth (vous ne faites que passer 0 à chaque fois)
Rich S
2

Il existe une autre façon de mesurer précisément les limites du texte, vous devez d'abord obtenir le chemin d'accès à la peinture et au texte actuels. Dans votre cas, ça devrait être comme ça:

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

Après cela, vous pouvez appeler:

mPath.computeBounds(mBoundsPath, true);

Dans mon code, il renvoie toujours les valeurs correctes et attendues. Mais, je ne sais pas si cela fonctionne plus rapidement que votre approche.

romain
la source
J'ai trouvé cela utile car je faisais de toute façon getTextPath, afin de dessiner un contour de trait autour du texte.
ToolmakerSteve
2

La différence entre getTextBoundset measureTextest décrite avec l'image ci-dessous.

En bref,

  1. getTextBoundsest d'obtenir le RECT du texte exact. Le measureTextest la longueur du texte, y compris l'espace supplémentaire à gauche et à droite.

  2. S'il y a des espaces entre le texte, il est mesuré measureTextdans la longueur des TextBounds, mais sans l'inclure, bien que les coordonnées soient décalées.

  3. Le texte peut être incliné (Inclinaison) vers la gauche. Dans ce cas, le côté gauche de la zone de délimitation dépasserait en dehors de la mesure du measureText, et la longueur totale du texte lié serait supérieure àmeasureText

  4. Le texte peut être incliné (Inclinaison) vers la droite. Dans ce cas, le côté droit de la zone de délimitation dépasserait en dehors de la mesure du measureText, et la longueur totale du texte lié serait supérieure àmeasureText

entrez la description de l'image ici

Elye
la source
0

Voici comment j'ai calculé les dimensions réelles de la première lettre (vous pouvez modifier l'en-tête de la méthode en fonction de vos besoins, c'est-à-dire au lieu de l' char[]utiliser 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;
}

Notez que j'utilise TextPaint au lieu de la classe Paint d'origine.

milosmns
la source