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