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

191

Korzystam z tekstu Paint.getTextBounds(), ponieważ jestem zainteresowany uzyskaniem zarówno wysokości, jak i szerokości tekstu do renderowania. Jednak rzeczywisty tekst renderowane jest zawsze nieco szerszy niż .width()w Rectinformacji wypełnionej przez getTextBounds().

Ku mojemu zaskoczeniu przetestowałem .measureText()i stwierdziłem, że zwraca inną (wyższą) wartość. Spróbowałem i uznałem, że jest poprawny.

Dlaczego zgłaszają różne szerokości? Jak prawidłowo uzyskać wysokość i szerokość? Mam na myśli, że mogę korzystać .measureText(), ale wtedy nie wiedziałbym, czy powinienem zaufać .height()oddanemu getTextBounds().

Zgodnie z prośbą, oto minimalny kod do odtworzenia problemu:

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

Dane wyjściowe pokazują, że różnica nie tylko staje się większa niż 1 (i nie jest to błąd zaokrąglania w ostatniej chwili), ale wydaje się również, że rośnie wraz z rozmiarem (miałem już wyciągnąć więcej wniosków, ale może być całkowicie zależna od czcionki):

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
źródło
Możesz zobaczyć [moja droga] [1]. To może uzyskać pozycję i prawidłowe związanie. [1]: stackoverflow.com/a/19979937/1621354
Alex Chi
Powiązane wyjaśnienie dotyczące metod pomiaru tekstu dla innych osób odwiedzających to pytanie.
Suragch

Odpowiedzi:

370

Możesz zrobić to, co zrobiłem, aby sprawdzić taki problem:

Studiuj kod źródłowy Androida, źródło Paint.java, zobacz zarówno metody MeasureText, jak i getTextBounds. Dowiesz się, że MeasureText wywołuje native_measureText, a getTextBounds wywołuje nativeGetStringBounds, które są metodami natywnymi zaimplementowanymi w C ++.

Więc kontynuowałbyś studiowanie Paint.cpp, który implementuje oba.

native_measureText -> SkPaintGlue :: MeasureText_CII

nativeGetStringBounds -> SkPaintGlue :: getStringBounds

Teraz twoje badanie sprawdza, gdzie różnią się te metody. Po kilku sprawdzeniach parametrów obie funkcje wywołują SkPaint :: MeasureText w Skia Lib (część Androida), ale obie wywołują różne przeciążone formularze.

Zagłębiając się w Skia, widzę, że oba wywołania dają to samo obliczenie w tej samej funkcji, tylko zwracają wynik inaczej.

Aby odpowiedzieć na twoje pytanie: oba połączenia wykonują te same obliczenia. Możliwa różnica wyniku polega na tym, że getTextBounds zwraca granice jako liczbę całkowitą, podczas gdy MeasureText zwraca wartość zmiennoprzecinkową.

Otrzymujesz więc błąd zaokrąglania podczas konwersji float na int, i dzieje się tak w Paint.cpp w SkPaintGlue :: doTextBounds w wywołaniu funkcji SkRect :: roundOut.

Różnica między obliczoną szerokością tych dwóch wywołań może wynosić maksymalnie 1.

EDYCJA 4 października 2011 r

Co może być lepszego niż wizualizacja. Podjąłem wysiłek, by odkrywać własne rzeczy i zasługiwać na nagrodę :)

wprowadź opis zdjęcia tutaj

Jest to wielkość czcionki 60, w kolorze czerwonym to granice prostokąta w fioletowy jest wynikiem measureText.

Widoczne jest, że ograniczona lewa część zaczyna niektóre piksele od lewej, a wartość MeasureText jest zwiększana o tę wartość zarówno po lewej, jak i po prawej stronie. Jest to coś, co nazywa się wartością AdvanceX Glypha. (Odkryłem to w źródłach Skia w SkPaint.cpp)

Wynik testu jest taki, że miara dodaje do tekstu po obu stronach pewną zaawansowaną wartość, podczas gdy getTextBounds oblicza minimalne granice, w których zmieści się dany tekst.

Mam nadzieję, że ten wynik Ci się przyda.

Kod testowy:

  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);
  }
Wskaźnik Null
źródło
3
Przepraszam, że tak długo komentowałem, że miałem pracowity tydzień. Dziękujemy za poświęcenie czasu na zagłębienie się w kod C ++! Niestety, to różnica jest większa niż 1, i to występuje nawet wtedy, gdy za pomocą wartości zaokrąglone - np. Rozmiaru tekstu ustawienie 29,0 powoduje measureText () = 263 i getTextBounds (szerokość) = 260
slezica
Proszę zamieścić minimalny kod z ustawieniem i pomiarem farby, który może odtworzyć problem.
Pointer Null
Zaktualizowano Jeszcze raz przepraszam za opóźnienie.
slezica
Jestem tutaj z Richiem. Świetne badania! Nagroda w pełni zasłużona i przyznana. Dziękujemy za poświęcony czas i wysiłek!
slezica
16
@ Myszy Właśnie znalazłem to pytanie ponownie (jestem OP). Zapomniałem, jak niewiarygodna była twoja odpowiedź. Dzięki z przyszłości! Najlepszy 100rp, jaki zainwestowałem!
Slezica
21

Moje doświadczenie z tym polega na tym, że getTextBoundszwróci ten absolutny minimalny prostokąt ograniczający, który otacza tekst, niekoniecznie zmierzoną szerokość używaną podczas renderowania. Chcę również powiedzieć, że measureTextzakłada jedną linię.

Aby uzyskać dokładne wyniki pomiaru, użyj przycisku StaticLayoutdo renderowania tekstu i wyciągnij pomiary.

Na przykład:

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();
Pościg
źródło
Nie wiedziałem o StaticLayout! getWidth () nie działa, po prostu zwraca to, co przekazuję w costructor (co widthnie jest maxWidth), ale getLineWith () zadziała. Znalazłem również metodę statyczną getDesiredWidth (), chociaż nie ma równoważnej wysokości. Popraw odpowiedź! Chciałbym też naprawdę wiedzieć, dlaczego tak wiele różnych metod daje różne wyniki.
slezica
Moje złe co do szerokości. Jeśli chcesz szerokość tekstu, który będzie renderowany w jednym wierszu, measureTextpowinna działać dobrze. W przeciwnym razie, jeśli znasz ograniczenia szerokości (takie jak w onMeasurelub onLayout, możesz użyć rozwiązania, które zamieściłem powyżej. Czy próbujesz automatycznie zmienić rozmiar tekstu? Ponadto, granica granic tekstu jest zawsze nieco mniejsza, ponieważ wyklucza to uzupełnianie znaków , tylko najmniejszy prostokąt z ogranicznikiem potrzebny do rysowania
Chase
Rzeczywiście próbuję automatycznie zmienić rozmiar TextView. Potrzebuję również wysokości, od tego zaczęły się moje problemy! MeasureWidth () działa inaczej, w przeciwnym razie. Po tym, mam ciekawy, dlaczego różne podejścia dał różne wartości
slezica
1
Należy również zauważyć, że widok tekstu, który jest zawijany i jest multilinią, mierzy w match_parent szerokość, a nie tylko wymaganą szerokość, co sprawia, że ​​powyższa szerokość param do StaticLayout jest poprawna. Jeśli potrzebujesz zmiany rozmiaru tekstu, sprawdź mój post na stackoverflow.com/questions/5033012/...
Chase 5'11
Musiałem mieć animację rozwijania tekstu, co pomogło wielu! Dziękuję Ci!
pudełko
18

Odpowiedź myszy jest świetna ... A oto opis prawdziwego problemu:

Krótka prosta odpowiedź brzmi: Paint.getTextBounds(String text, int start, int end, Rect bounds)zwroty, Rectktóre nie zaczynają się od (0,0). Oznacza to, że aby uzyskać rzeczywistą szerokość tekstu, który zostanie ustawiony przez wywołanie Canvas.drawText(String text, float x, float y, Paint paint)tego samego obiektu Paint z getTextBounds (), należy dodać lewą pozycję Rect. Coś w tym stylu:

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

Zauważ to bounds.left- to klucz do problemu.

W ten sposób otrzymasz taką samą szerokość tekstu, jak przy użyciu Canvas.drawText().

I ta sama funkcja powinna być do uzyskania heighttekstu:

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: Nie testowałem dokładnie tego kodu, ale przetestowałem koncepcję.


W tej odpowiedzi podano o wiele bardziej szczegółowe wyjaśnienie .

Prizoff
źródło
2
Rect definiuje (b. Szerokość) na (b. Prawo - b. Lewa) i (b. Wysokość) na (b. Dolna - b. Górna). Dlatego możesz zastąpić (b. Lewy + b. Szerokość) na (b. Prawy) i (b. Dolny + b. Wzrost) na (2 * b. Dolny-b.top), ale myślę, że chcesz mieć ( b.bottom-b.top) zamiast tego, co jest tym samym, co (b.height). Myślę, że masz rację co do szerokości (w oparciu o sprawdzenie źródła C ++), getTextWidth () zwraca tę samą wartość co (b.right), mogą wystąpić pewne błędy zaokrąglania. (b to odniesienie do granic)
Nulano
11

Przepraszam, że odpowiedziałem ponownie na to pytanie ... Musiałem osadzić obraz.

Myślę, że wyniki znalezione przez myszy są mylące. Obserwacje mogą być poprawne dla rozmiaru czcionki 60, ale zmieniają się znacznie bardziej, gdy tekst jest mniejszy. Na przykład. 10px. W takim przypadku tekst jest rysowany POZA granicami.

wprowadź opis zdjęcia tutaj

Kod źródłowy zrzutu ekranu:

  @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
źródło
Ach, tajemnica ciągnie się dalej. Nadal mnie to interesuje, nawet z ciekawości. Daj mi znać, jeśli znajdziesz coś jeszcze!
slezica
Problemu można trochę zaradzić, mnożąc rozmiar czcionki, który chcesz zmierzyć, powiedzmy 20, a następnie podziel przez to wynik. NIŻ możesz chcieć dodać o 1-2% większą szerokość mierzonego rozmiaru, aby być po bezpiecznej stronie. Na końcu będziesz mieć tekst mierzony jako długi, ale przynajmniej tekst nie będzie się nakładał.
Moritz
6
Aby narysować tekst dokładnie wewnątrz prostokąta ograniczającego, nie wolno rysować tekstu z X = 0,0f, ponieważ prostokąty ograniczające zwracają się jak prostokąt, a nie szerokość i wysokość. Aby narysować go poprawnie, należy przesunąć współrzędną X na wartość „-bounds.left”, a następnie zostanie pokazana poprawnie.
Rzym.
10

ZASTRZEŻENIE: To rozwiązanie nie jest w 100% dokładne pod względem określenia minimalnej szerokości.

Zastanawiałem się też, jak mierzyć tekst na płótnie. Po przeczytaniu świetnego postu od myszy miałem problemy z mierzeniem tekstu wielowierszowego. Nie ma oczywistej drogi od tych wkładów, ale po przeprowadzeniu pewnych badań przeprowadzam przeglądanie klasy StaticLayout. Pozwala mierzyć tekst wielowierszowy (tekst za pomocą „\ n”) i konfigurować znacznie więcej właściwości tekstu za pomocą skojarzonego z nim programu Paint.

Oto fragment pokazujący, jak mierzyć tekst wielowierszowy:

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

Wrapwitdh jest w stanie określić, czy chcesz ograniczyć tekst wielowierszowy do określonej szerokości.

Ponieważ StaticLayout.getWidth () zwraca tylko to boundedWidth, musisz zrobić kolejny krok, aby uzyskać maksymalną szerokość wymaganą przez tekst wielowierszowy. Jesteś w stanie określić szerokość każdej linii, a maksymalna szerokość jest oczywiście najwyższą szerokością linii:

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
źródło
nie powinieneś wchodzić ido getLineWidth (za każdym razem po prostu podajesz 0)
Rich S
2

Istnieje inny sposób precyzyjnego pomiaru granic tekstu, najpierw powinieneś uzyskać ścieżkę dla bieżącej farby i tekstu. W twoim przypadku powinno być tak:

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

Następnie możesz zadzwonić:

mPath.computeBounds(mBoundsPath, true);

W moim kodzie zawsze zwraca poprawne i oczekiwane wartości. Ale nie jestem pewien, czy działa szybciej niż twoje podejście.

rzymski
źródło
Uznałem to za przydatne, ponieważ i tak robiłem getTextPath, aby narysować kontur obrysu wokół tekstu.
ToolmakerSteve
1

Różnice między getTextBoundsi measureTextopisano na poniższym obrazku.

W skrócie,

  1. getTextBoundsjest uzyskanie RECT dokładnego tekstu. Jest measureTextto długość tekstu, w tym dodatkowa przerwa po lewej i prawej stronie.

  2. Jeśli między tekstem są spacje, jest mierzone, measureTextale nie obejmuje długości TextBounds, chociaż współrzędna zostaje przesunięta.

  3. Tekst można przechylić (pochylenie) w lewo. W takim przypadku obwiednia po lewej stronie przekroczyłaby wartość pomiaru MeasureText, a całkowita długość oprawionego tekstu byłaby większa niżmeasureText

  4. Tekst może być przechylony (pochylenie) w prawo. W takim przypadku obwiednia po prawej stronie przekroczyłaby wartość pomiaru MeasureText, a całkowita długość oprawionego tekstu byłaby większa niżmeasureText

wprowadź opis zdjęcia tutaj

Elye
źródło
0

W ten sposób obliczyłem rzeczywiste wymiary pierwszej litery (możesz zmienić nagłówek metody według własnych potrzeb, tj. Zamiast char[]używać 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;
}

Zauważ, że używam TextPaint zamiast oryginalnej klasy Paint.

milosmny
źródło