Android Paint:.measureText() и getTextBounds()


Я измеряю текст с помощью 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
7 170

7 ответов:

вы можете сделать то, что я сделал, чтобы проверить такую проблему:

изучение исходного кода Android, краска.источник java, см. Оба метода measureText и getTextBounds. Вы узнаете, что measureText вызывает native_measureText, а getTextBounds вызывает nativeGetStringBounds, которые являются собственными методами, реализованными в C++.

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

native_measureText -> SkPaintGlue:: measureText_CII

nativeGetStringBounds -> SkPaintGlue:: getStringBounds

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

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

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

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

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

EDIT 4 Oct 2011

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

enter image description here

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

видно, что левая часть границ начинается с нескольких пикселей слева, и значение measureText увеличивается на это значение как слева, так и справа. Это уже что-то называется значением AdvanceX глифа. (Я обнаружил это в источниках 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);
  }

мой опыт заключается в том, что 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();

ответ мыши велик... А вот описание реальной проблемы:

короткий простой ответ заключается в том, что 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;
}

P. s.: Я не тестировал этот точный код, но проверил концепцию.


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

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

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

enter image description here

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

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

отказ от ответственности: данное решение не является 100% точным с точки зрения определения минимальной ширины.

Я также выяснял, как измерить текст на холсте. После прочтения Великого поста от мышей у меня были некоторые проблемы о том, как измерить многострочный текст. Нет очевидного способа из этих вкладов, но после некоторых исследований я cam через класс StaticLayout. Он позволяет измерять многострочный текст (текст с "\n") и настраивать гораздо больше свойств вашего текста через соответствующую краску.

вот фрагмент, показывающий, как измерить многострочный текст:

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( 0 ) > maxLine ) {
            maxLine = layout.getLineWidth( 0 );
        }
    }
    return maxLine;
}

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

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

после этого вы можете позвонить:

mPath.computeBounds(mBoundsPath, true);

в моем коде он всегда возвращает правильные и ожидаемые значения. Но, не уверен, что это работает быстрее, чем ваш подход.

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