Android Paint: .measureText () против .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, ознакомьтесь с методами measureText и getTextBounds. Вы узнаете, что measureText вызывает native_measureText, а getTextBounds вызывает nativeGetStringBounds, которые являются нативными методами, реализованными в C ++.

Таким образом, вы продолжите изучать Paint.cpp, который реализует оба.

native_measureText -> SkPaintGlue :: measureText_CII

nativeGetStringBounds -> SkPaintGlue :: getStringBounds

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

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

Чтобы ответить на ваш вопрос: оба ваших звонка выполняют одинаковые вычисления. Возможное различие результата заключается в том, что getTextBounds возвращает границы как целое число, а measureText возвращает значение с плавающей запятой.

Итак, вы получаете ошибку округления во время преобразования float в int, и это происходит в Paint.cpp в SkPaintGlue :: doTextBounds при вызове функции SkRect :: roundOut.

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

РЕДАКТИРОВАТЬ 4 октября 2011

Что может быть лучше, чем визуализация. Я приложил усилия, чтобы исследовать себя и заслужить награду :)

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

Это размер шрифта 60, в красный цвет границы прямоугольника, в фиолетовый результат measureText.

Видно, что границы левой части начинаются на несколько пикселей слева, и значение measureText увеличивается на это значение как слева, так и справа. Это то, что называется значением AdvanceX в Glyph. (Я обнаружил это в источниках Skia в SkPaint.cpp)

Таким образом, результатом теста является то, что measureText добавляет некоторое предварительное значение к тексту с обеих сторон, в то время как 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 приводит к measureText () = 263 и getTextBounds ().
Width

Пожалуйста, опубликуйте минимальный код с настройкой и измерением Paint, который может воспроизвести проблему.
Указатель нулевой

Обновлено. Извините еще раз за задержку.
Slezica

Я здесь с Ричем. Отличное исследование! Баунти полностью заслужен и награжден. Спасибо за ваше время и усилия!
Slezica

16
@ Мыши я только что нашел этот вопрос снова (я OP). Я забыл, насколько невероятным был твой ответ. Спасибо из будущего! Лучшие 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. Мне тоже нужна высота, вот тут и начались мои проблемы! measureWidth () отлично работает в противном случае. После этого мне стало интересно, почему разные подходы дают разные значения
slezica

1
Следует также отметить, что текстовое представление, которое переносится и является многострочным, будет измерять в match_parent ширину, а не только требуемую ширину, что делает приведенный выше параметр ширины в StaticLayout допустимым. Если вам нужно изменить размер текста, прочитайте мой пост на stackoverflow.com/questions/5033012/…
Погоня

Мне нужно было иметь анимацию расширения текста, и это помогло куче! Спасибо!
коробка

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

PS: Я не проверял этот точный код, но проверил концепцию.


Более подробное объяснение дано в этом ответе.


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), могут быть некоторые ошибки округления. (б - ссылка на границы)
Нулано,

11

Извините, что снова ответил на этот вопрос ... Мне нужно было вставить изображение.

Я думаю, что результаты, полученные @mice, вводят в заблуждение. Наблюдения могут быть правильными для размера шрифта 60, но они становятся намного более разными, когда текст меньше. Например. 10px. В этом случае текст на самом деле нарисован за пределами границ.

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

Исходный код скриншота:

  @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 () возвращает только эту boundedWidth, вы должны сделать еще один шаг, чтобы получить максимальную ширину, требуемую вашим многострочным текстом. Вы можете определить ширину каждой линии, а максимальная ширина - это максимальная ширина линии курса:

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это получить ПРАВО точного текста. measureTextДлина текста, в том числе дополнительного зазора слева и справа.

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

  3. Текст можно было наклонить (наклонить) влево. В этом случае левая сторона ограничительной рамки будет выходить за пределы измерения measureText, а общая длина границы текста будет больше, чемmeasureText

  4. Текст может быть наклонен (наклон) вправо. В этом случае правая сторона ограничивающего прямоугольника будет выходить за пределы измерения measureText, а общая длина границы текста будет больше, чем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.