1. Paint.getTextBounds 测量文本宽高
val text = "Hello, Mango !"
val bounds = Rect()
paint.getTextBounds(text, 0, text.length, bounds)
val textWidth = bounds.width() // 获取文本的宽度
val textHeight = bounds.height() // 获取文本的高度
// 确定文本的垂直边界
val textTop = bounds.top
val textBottom = bounds.bottom
//得到 text 的 宽 高 上 下 为:199 , 31, -24, 7
-
用来精确计算给定文本的矩形边界,返回的是文本的最小矩形框(即
Rect
对象)。 -
getTextBounds()
不仅仅测量文本的宽度,还包括了高度(即文本在垂直方向的大小)。
参数:
text
: 要测量的文本字符串。start
: 字符串的起始索引(通常是 0)。end
: 字符串的结束索引(通常是text.length
)。bounds
: 一个Rect
对象,用来接收测量后的矩形边界结果。
适用场景:
- 想知道文本的上、下、左、右边界
- 复杂的对齐需求(例如多个文本行的布局)
- 考虑文本的垂直对齐、边界检测等场景
缺点:
getTextBounds()
返回的 Rect
是基于实际字符内容的最小矩形框,它只测量文字的实际占用空间,但这个矩形可能不会考虑字体的全部空间,尤其是在处理字符的上升部分和下降部分时。
bounds.top
和bounds.bottom
仅限于当前文本的实际字符范围,而不会包含字体的完整高度。- 对于像 “g” 或 “y” 这样的字母,
getTextBounds()
的bounds.height()
可能无法涵盖字符的下降部分,因为它只关心文本的显示部分,不会测量到字体的潜在最大高度(例如字体设计中保留的额外空间),因此在某些情况下会裁剪字符的部分内容(特别是下方的部分)。
实际显示效果:
2. Paint.measureText 测量文本宽度
val text = "Hello, Mango !"
val textWidth = paint.measureText(text)
Paint.measureText()
,返回文本的宽度(以像素为单位), 且是float
,可以直接用于水平布局或绘制。
Tip:
measureText()
返回的宽度是文本所占据的水平空间,包含了每个字符的宽度和字符之间的间距。- 它只返回宽度,不返回高度。如果你需要获取文本的垂直尺寸(高度),则需要使用
Paint.getTextBounds()
或Paint.FontMetrics
。 - 主要用于水平布局或对齐时,只关心文本在水平方向上占据多少空间。
measureText 和 getTextBounds的区别
getTextBounds的区别
返回的是文字的实际宽度measureText
包含字符更多的额外空间
3. Paint.fontMetrics 测量文本高度
val fontMetrics = paint.fontMetrics
val bottom = fontMetrics.bottom
val top = fontMetrics.top
val ascent = fontMetrics.ascent
val descent = fontMetrics.descent
val leading = fontMetrics.leading // additional space to add between lines of text.
val textHeight = fontMetrics.bottom - fontMetrics.top
fontMetrics
用于获取字体的垂直尺寸信息(如 ascent、descent、leading 等),方便我们计算文本的总高度,以及处理文本基线对齐问题。
通过 fontMetrics
,你可以得到文本的完整高度,并考虑到字体的所有垂直偏移信息。
Paint.FontMetrics
是基于字体基线(baseline)来计算文本的高度,它可以考虑到字体的所有元素,包括上升部分(ascenders) 和 下降部分(descenders),例如字母 “g”、”y” 等带有下划线部分的字符。
参数详解
fontMetrics.top
字体中最上面的部分相对于基线的距离。表示字体最上方的那部分位置。
负值
以 baseline
为坐标轴,top
在baseline 上
方,所以为负。
它表示的是 top 到 baseline 的距离。
fontMetrics.bottom
字体的最底部,包含所有字母的下划部分(例如 “g”、”y” 的尾巴)。
正值
以 baseline
为坐标轴,top
在baseline 下
方,所以为负。
它表示的是 bottom 到 baseline 的距离。
fontMetrics.ascent
负值
字形中字符的顶部到基线的距离,表示字符上升的高度。
fontMetrics.descent
正值
字形中字符的底部到基线的距离,表示字符下降的高度。
4. baseline 的计算方法
什么是 baseline ?
基线是文字的绘制参考线,通过基线调整,可以让文字在垂直方向上正确对齐。
在 Android 的 FontMetrics 中,top
是负值,bottom
是正值,这是因为它们的参考点是 基线(baseline),而字体的垂直布局是相对于基线来进行计算的。
字体的垂直布局是以基线为中心的。
文字布局的垂直坐标系
我们可以把它想象成一个二维坐标系:
- 基线(baseline)是 Y=0 的位置,也就是垂直方向上的原点。
- 向上 的坐标是负的,表示字符顶部(
ascent
、top
)距离基线多远。 - 向下 的坐标是正的,表示字符底部(
descent
、bottom
)距离基线多远。
如何计算文字高度?
top 和 bottom 是字体在垂直方向上的极限值,分别代表了字体在垂直方向上的最高和最低位置, 因此:
textHeight = | top | + | bottom | // 因为 top 是负值,所以去掉绝对值符号后
= - top + bottom
= bottom - top
bottom - top
代表了整个文字的实际高度,包括所有可能的上下空间(如字母 “g” 的下垂部分或字母 “h” 的上伸部分)。
为什么不使用 ascent 和 descent ?
ascent
和descent
代表的是主要字符部分的高度,但不包括一些上部或者下部的额外空间(例如字母“g”或“p”的下垂部分,或者字体可能有的上方装饰部分)。- 使用
top
和bottom
可以确保你为整个字体的任何部分都预留了足够的空间,避免文字在垂直方向被裁切。
计算 baseline
在自定义 View 中绘制文字时,文字的绘制位置是从 基线(baseline) 开始的,而不是从文字的顶部或底部。因此,如果想让文字垂直居中,必须要先考虑基线的位置,再根据字体的高度来调整绘制的起点。
计算文字在自定义 View 中垂直居中的 Y 坐标,使得文字看起来位于 View 的中间:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val fontMetrics = paint.fontMetrics
val bottom = fontMetrics.bottom
val top = fontMetrics.top
val textHeight = bottom - top
val baseline = (height - textHeight) / 2 - top
canvas.drawText(text, 0f, baseline, paint)
}
height
是自定义 View 的高度,由父布局决定
它不是坐标值,而是View 从顶部到底部的垂直尺寸,单位是像素,是在布局时由父布局传递给子 View 的
在这里的 height
是 onMeasure()
里修改后的高度,也就是布局高度,在这里 布局高度 = 文字高度。
(height - textHeight) / 2
表示将多余的空白区域均匀分布在文字的上下两端,使文字垂直居中。
- top
view上方到text的空白 + text 顶部到基线的距离,即 | top | ,就是 基线(baseline)的 Y 坐标
baseline
是一个坐标值,为了确定文字相对于 View 的垂直居中位置
让MyTextView显示在屏幕中间
在代码中设置Gravity
val textView = MyTextView(this, null).apply {
setText("Hello, Mango !")
setTextSize(32f)
setBackgroundColor(Color.GRAY)
val params = FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
params.gravity = Gravity.CENTER
layoutParams = params // 设置 MyTextView 居中
}
将 MyTextView 设置为居中显示,无论 MyTextView 在屏幕的哪个位置,它的文本的坐标起始点都是那个文本Rect的左上角
因为我们在OnMeasure()里重新绘制了MyTextView的宽高,所以它只会是文本宽高大小
无论在代码中用LayoutParams怎么移动MyTextView,它的drawText起始点都是 0f, baseline
。
是否需要调用 super.onMeasure()?
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
-
如果你需要完全自定义测量逻辑,比如在
CustomTextView
中根据文本的内容动态决定视图的宽高,并且你已经在onMeasure()
方法内完成了宽高的计算(通过setMeasuredDimension()
来设置视图的测量宽高,已经足够告诉系统你希望视图的尺寸是多少),那么就不需要再调用super.onMeasure()
。这样,你的视图将根据你自己定义的测量逻辑来确定大小。 -
如果你的自定义视图依赖父类的一些测量逻辑(例如某些复杂视图继承自
ViewGroup
或其他带有默认测量行为的控件),你可能需要调用super.onMeasure()
来确保继承自父类的默认测量行为被执行。
在你的示例中,你已经完全控制了视图的测量逻辑,动态计算文本的宽高并设置给视图,因此不再需要调用 super.onMeasure()
。
示例:动态根本文本内容控制宽高
class CustomTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var customText: String = "Hello, Custom TextView!"
private var customTextColor: Int = Color.BLACK
private var customTextSize: Float = 50f
private val paint = Paint()
init {
paint.color = customTextColor
paint.textSize = customTextSize
paint.isAntiAlias = true
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 1. 获取文本宽高
val textWidth = paint.measureText(customText)
val fontMetrics = paint.fontMetrics
val textHeight = fontMetrics.bottom - fontMetrics.top
// 2. 获取宽高模式
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
// 3. 默认的宽高
var desiredWidth = textWidth.toInt() + paddingLeft + paddingRight
var desiredHeight = textHeight.toInt() + paddingTop + paddingBottom
// 4. 根据模式处理宽高测量逻辑
val width = when (widthMode) {
MeasureSpec.EXACTLY -> MeasureSpec.getSize(widthMeasureSpec) // 父布局要求精确大小
MeasureSpec.AT_MOST -> minOf(MeasureSpec.getSize(widthMeasureSpec), desiredWidth) // 父布局设置了最大值,我们取较小的那个
MeasureSpec.UNSPECIFIED -> desiredWidth // 没有限制时使用测量的文本宽度
else -> desiredWidth
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> MeasureSpec.getSize(heightMeasureSpec) // 父布局要求精确大小
MeasureSpec.AT_MOST -> minOf(MeasureSpec.getSize(heightMeasureSpec), desiredHeight) // 父布局设置了最大值,我们取较小的那个
MeasureSpec.UNSPECIFIED -> desiredHeight // 没有限制时使用测量的文本高度
else -> desiredHeight
}
// 5. 设置最终测量的宽高
setMeasuredDimension(width, height)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 计算绘制文本的位置
val x = (width - paint.measureText(customText)) / 2
val y = (height / 2) - (paint.descent() + paint.ascent()) / 2
// 绘制文本
canvas.drawText(customText, x, y, paint)
}
}
typography
Serif
: 衬线,图1红色部分
Sans Serif
: Sans
是 没有
的意思,即 font without serif
Tracking
:字符之间的空间
Kerning
:字距调整