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


191

Я вимірюю текст за допомогою Paint.getTextBounds(), оскільки мені цікаво отримати як висоту, так і ширину тексту для виведення. Однак сам текст виявляється завжди трохи ширше , ніж .width()в Rectінформації , заповненої getTextBounds().

На моє здивування, я перевірив .measureText()і виявив, що він повертає інше (вище) значення. Я спробував це і визнав правильно.

Чому вони повідомляють різну ширину? Як я можу правильно отримати висоту та ширину? Я маю на увазі, я можу використовувати .measureText(), але тоді я б не знав, чи варто довіряти .height()поверненим getTextBounds().

Як вимагається, тут є мінімальний код для відтворення проблеми:

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

Результат показує, що різниця не тільки стає більшою за 1 (і не є помилкою округлення в останню хвилину), але також, здається, збільшується з розміром (я збирався зробити більше висновків, але це може бути повністю залежно від шрифту):

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

Ви можете бачити [мій шлях] [1]. Це може отримати позицію і виправити прив'язку. [1]: stackoverflow.com/a/19979937/1621354
Алекс Чи

Пов'язане пояснення щодо методів вимірювання тексту для інших відвідувачів цього питання.
Сурагч

Відповіді:


370

Ви можете зробити те, що я зробив, щоб перевірити таку проблему:

Вивчіть вихідний код Android, джерело Paint.java, перегляньте методи вимірюванняText та getTextBounds. Ви дізнаєтесь, що ukreText викликає native_measureText, а getTextBounds викликає nativeGetStringBounds, які є нативними методами, реалізованими в C ++.

Отже, ви продовжите вивчати Paint.cpp, який реалізує і те і інше.

native_measureText -> SkPaintGlue :: мераText_CII

nativeGetStringBounds -> SkPaintGlue :: getStringBounds

Тепер ваше дослідження перевіряє, де ці методи відрізняються. Після декількох перевірок парами обидва виклику функціонують SkPaint :: мераText у Skia Lib (частина Android), але вони обидва викликають різну перевантажену форму.

Заглиблюючись далі в Skia, я бачу, що обидва виклики призводять до однакових обчислень в одній і тій же функції, лише повертають результат по-різному.

Щоб відповісти на ваше запитання: Обидва ваші дзвінки роблять однакові обчислення. Можлива різниця результату полягає в тому, що getTextBounds повертає межі у вигляді цілого числа, тоді як вимірюєText повертає значення.

Отже, ви отримуєте помилку округлення під час перетворення float в int, і це відбувається в Paint.cpp в SkPaintGlue :: doTextBounds при виклику для функціонування SkRect :: roundOut.

Різниця між обчисленою шириною цих двох викликів може бути максимально 1.

EDIT 4 жовтня 2011 р

Що може бути краще, ніж візуалізація. Я взяв на себе зусилля, як для власного дослідження, так і для заслугової щедрості :)

введіть тут опис зображення

Це розмір шрифту 60, в червоний колір кордону прямокутника, в фіолетовий результат measureText.

Видно, що ліва частина меж починає деякі пікселі зліва, а значення mereText збільшується на це значення і зліва, і справа. Це щось, що називається значенням AdvanceX Glyph. (Я виявив це у джерелах Skia у SkPaint.cpp)

Отже, результат тесту полягає в тому, що мераText додає тексту попереднього значення з обох сторін, тоді як getTextBounds обчислює мінімальні межі, куди буде відповідати даний текст.

Сподіваюся, що цей результат вам корисний.

Код тестування:

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

3
Вибачте за те, що я так довго коментував, у мене був тиждень напружений. Дякуємо, що знайшли час, щоб розібратися в коді C ++! На жаль, різниця більша за 1, і вона виникає навіть при використанні округлих значень - наприклад, встановлення розміру тексту на 29,0 призводить до mereText () = 263 та getTextBounds (). Width = 260
slezica

Будь ласка, опублікуйте мінімальний код із налаштуванням та вимірюванням фарби, який може відтворити проблему.
Pointer Null

Оновлено. Вибачте ще раз за затримку.
slezica

Я тут з Річком. Прекрасне дослідження! Баунті цілком заслужено та нагороджено. Дякую за ваш час та зусилля!
slezica

16
@mice Я щойно знайшов це запитання ще раз (я ОП). Я забув, наскільки неймовірна ваша відповідь. Дякую від майбутнього! Найкращий 100рр я вклав!
slezica

21

Мій досвід із цим полягає в тому getTextBounds, що повернемося до абсолютної мінімальної обмежувальної прямої, яка інкапсулює текст, не обов'язково вимірюваної ширини, що використовується при візуалізації. Я також хочу це сказатиmeasureText передбачає один рядок.

Для отримання точних результатів вимірювань слід скористатися StaticLayout текстом і витягнути вимірювання.

Наприклад:

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

Не знав про StaticLayout! getWidth () не буде працювати, він просто повертає те, що я передаю в інструкторі (що це widthне так maxWidth), але getLineWith () буде. Я також знайшов статичний метод getDesiredWidth (), хоча немає еквіваленту висоти. Будь ласка, виправте відповідь! Також я хотів би знати, чому так багато різних методів дають різні результати.
slezica

Моє погано щодо ширини. Якщо ви хочете, щоб ширина якогось тексту виводилася в один рядок, це measureTextмає працювати добре. В іншому випадку, якщо ви знаєте вміст ширини (наприклад, в, onMeasureабо onLayoutви можете використовувати рішення, яке я розмістив вище. Ви намагаєтесь автоматично змінити розмір тексту? Також причина меж тексту завжди трохи менша, тому що вона виключає будь-яку прокладку символів , лише найменший обмежений прямокутник, необхідний для малювання.
Чейз

Я справді намагаюся автоматично змінити розмір TextView. Мені потрібен і зріст, саме тут почалися мої проблеми! мераWidth () працює нормально інакше. Після цього мені стало цікаво, чому різні підходи давали різні значення
slezica

1
Варто також зазначити, що текстове перегляд, яке обгортає та є багаторядковим, вимірюватиме на match_parent для ширини, а не лише необхідної ширини, що робить вищезгаданий параметр ширини для StaticLayout дійсним. Якщо вам потрібен редактор тексту, перегляньте мою публікацію на сайті stackoverflow.com/questions/5033012/…
Chase

Мені потрібно було анімацію розширення тексту, і це допомогло купу! Дякую!
поле

18

Відповідь мишей чудова ... І ось опис реальної проблеми:

Коротка проста відповідь - це те, що Paint.getTextBounds(String text, int start, int end, Rect bounds)повертається, Rectяке не починається (0,0). Тобто, щоб отримати фактичну ширину тексту, яка буде встановлена ​​за допомогою дзвінкаCanvas.drawText(String text, float x, float y, Paint paint) з тим самим об’єктом Paint з getTextBounds (), слід додати ліву позицію Rect. Щось схоже:

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

Зауважте це bounds.left - ось ключ проблеми.

Таким чином ви отримаєте ту саму ширину тексту, яку ви отримали б за допомогою Canvas.drawText() .

І та сама функція повинна бути для отримання height тексту:

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

Пс: Я не перевіряв цього точного коду, але тестував його.


Набагато детальніше пояснення дано у цій відповіді.


2
Rect визначає (b.width) бути (b.right-b.left) і (b.height) бути (b.bottom-b.top). Тому ви можете замінити (b.left + b.width) на (b.right) і (b.bottom + b.height) на (2 * b.bottom-b.top), але я думаю, ви хочете мати ( b.bottom-b.top) замість того, що те саме, що (b.height). Я думаю, що ви правильні щодо ширини (на основі перевірки джерела C ++), getTextWidth () повертає те саме значення, що і (b.right), можуть бути деякі помилки округлення. (b - посилання на межі)
Нулано,

11

Вибачте за те, що знову відповіли на це питання ... Мені потрібно було вставити зображення.

Я думаю, що результати @mice знайдені помилковими. Спостереження можуть бути правильними щодо розміру шрифту 60, але вони виходять набагато різними, коли текст менший. Напр. 10 пікс. У такому випадку текст насправді намальований ЗА МЕЖАМИ.

введіть тут опис зображення

Вихідний код скріншоту:

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

Ах, містерія затягується. Мені все ще цікаво це, навіть якщо з цікавості. Дайте мені знати, якщо ви дізнаєтесь щось інше, будь ласка!
slezica

Проблему можна трохи усунути, коли ви помножите розмір шрифтів, який ви хочете виміряти, на коефіцієнт, що дозволяє сказати, 20 і чим ділити результат на це. ТОМУ ви також можете додати на 1-2% більше ширини вимірюваного розміру, щоб бути на безпечній стороні. Зрештою, у вас з'явиться текст, який вимірюється як довгий, але принаймні немає тексту, що перетинається.
Моріц

6
Щоб намалювати текст точно всередині обмежуючої прямої, ви не повинні малювати Текст з X = 0,0f, тому що обмежувальна пряма повертається як пряма, а не як ширина та висота. Щоб правильно намалювати його, слід змістити координату X на значення "-bounds.left", тоді вона буде показана правильно.
Роман

10

ВИСНОВОК: Це рішення не є на 100% точним щодо визначення мінімальної ширини.

Я також розбирався, як виміряти текст на полотні. Прочитавши чудовий допис від мишей, у мене виникли деякі проблеми щодо вимірювання багаторядкового тексту. Немає очевидного шляху з цих внесків, але після деяких досліджень я переходжу через клас StaticLayout. Це дозволяє вимірювати багаторядковий текст (текст з "\ n") та налаштовувати набагато більше властивостей тексту за допомогою пов'язаної Paint.

Ось фрагмент, який показує, як вимірювати багаторядковий текст:

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 здатний визначити, чи потрібно обмежувати багаторядковий текст певною шириною.

Оскільки StaticLayout.getWidth () повертає лише цю обмежену ширину, ви повинні зробити ще один крок, щоб отримати максимальну ширину, необхідну для вашого багаторядкового тексту. Ви можете визначити ширину кожного рядка, а максимальна ширина - найвища ширина рядка.

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

якщо ви не переходите iв getLineWidth (ви щоразу проходите 0)
Rich S

2

Існує ще один спосіб точно виміряти межі тексту, спочатку слід отримати шлях для поточної фарби та тексту. У вашому випадку має бути так:

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

Після цього ви можете зателефонувати:

mPath.computeBounds(mBoundsPath, true);

У моєму коді він завжди повертає правильні та очікувані значення. Але не впевнений, чи працює він швидше, ніж ваш підхід.


Я вважаю це корисним, оскільки я все-таки робив getTextPath, щоб намалювати контур тексту.
ToolmakerSteve

2

Різне між getTextBoundsі measureTextописано із зображенням нижче.

Коротко,

  1. getTextBoundsце отримати РЕКТ точного тексту. ThemeasureTextДовжина тексту, в тому числі додаткового зазору зліва і справа.

  2. Якщо між текстом є пробіли, він вимірюється в measureText але не враховує довжину TextBounds, хоча координата зміщується.

  3. Текст можна нахилити (нахилити) вліво. У цьому випадку лівий бік обмежувального поля перевищуватиме би показник вимірюванняText, а загальна довжина пов'язаного тексту була б більшою, ніжmeasureText

  4. Текст можна нахилити (нахилити) вправо. У цьому випадку правий бік обмежувального поля буде перевищувати показник вимірюванняText, і загальна довжина вв'язаного тексту буде більшою, ніжmeasureText

введіть тут опис зображення


0

Ось як я обчислив реальні розміри для першої літери (ви можете змінити заголовок методу відповідно до ваших потреб, тобто замість char[]використання 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;
}

Зауважте, що я використовую TextPaint замість оригінального класу Paint.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.