Paint.measureText() 测量时的一个隐藏Bug
有这么一个场景:需要根据文本内容动态调整布局,首先需要计算文本行,开始使用的是 Paint.measureText() 来计算行数,示例代码如下:
private val mTv: TextView by lazy { findViewById(R.id.xxx)}
fun calculateLineCount(): Int {
val measureTxtStr = "xxxxxx"
val paint = mTv.paint
paint.run {
textSize = mTv.textSize
typeface = mTv.typeface
}
val totalWidth = paint.measureText(measureTxtStr) //通过measureText测量出总长度
val perLineWidth = 300.dp2px() //假如每行宽度是300dp
val lineCount = if (totalWidth <= perLineWidth) 1 else ceil(totalWidth / perLineWidth).toInt()
return lineCount
}
这段代码看起来逻辑清晰,但可能会出现测量错误。
换行符的陷阱:
Paint.measureText() 会将换行符 \n 当作普通字符处理,而不是布局指令。这意味着:
val singleLine = "这是一行文本"
val multiLine = "这是第一行\n这是第二行"
measureText 会把 \n 也计算进了总宽度而没有特殊处理,比如上述multiLine本来是要展示两行的,但是通过paint.measureText测量完之后计算出来可能只有一行导致计算错误。
StaticLayout 来实现
为了解决上述问题,可以通过StaticLayout来测量, StaticLayout是 Android 专门用于文本布局的类,它会将 \n 视为换行指令。除此之外,StaticLayout还会考虑排版规则:包括对齐、间距、字体等,所以StaticLayout能提供精确布局信息:行数、每行高度、宽度等信息。以下是修改后的代码:
private val mTv: TextView by lazy { findViewById(R.id.xxx)}
val measureTxtStr = "这是第一行\n这是第二行"
val paint = mTv.paint
paint.run {
textSize = mTv.textSize
typeface = mTv.typeface
}
//使用post确保在布局完成后执行
mTv.post {
val paint = mTv.paint.apply {
textSize = mTv.textSize
typeface = mTv.typeface
}
val perLineWidth = 300.dp2px() //假如每行宽度是300dp
//使用StaticLayout计算行数
val lineCount = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//Android 6.0+使用
val staticLayout = StaticLayout.Builder
.obtain(measureTxtStr, 0, measureTxtStr.length, paint, perLineWidth)
.setLineSpacing(0f, 1.0f) // 设置行间距:加值0,倍数1
.build()
staticLayout.lineCount
} else {
// 兼容旧版本
@Suppress("DEPRECATION")
val staticLayout = StaticLayout(
measureTxtStr,
paint,
perLineWidth,
Layout.Alignment.ALIGN_NORMAL, // 对齐方式
1.0f, // 行间距倍数
0f, // 行间距加值
false // 是否包含内边距
)
staticLayout.lineCount
}
//现在lineCount是精确的行数,可以根据这个行数进行后续布局调整
}
封装成工具函数
为了方便使用,我们可以将上述逻辑封装成工具函数:
object TextViewUtils {
/**
* 计算TextView的行数并回调结果
*/
fun processTextViewLineCount(
textView: TextView,
text: CharSequence,
availableWidth: Int,
callback: (lineCount: Int) -> Unit
) {
// 先设置文本
textView.text = text
// 在布局完成后计算
textView.post {
val lineCount = calculateTextLineCount(textView, text, availableWidth)
callback(lineCount)
}
}
/**
* 计算TextView中文本的行数
* @param text 文本内容
* @param availableWidth 可用宽度
* @return 文本在指定宽度下的行数
*/
private fun calculateTextLineCount(
textView: TextView,
text: CharSequence,
availableWidth: Int
): Int {
// 获取TextView的画笔配置
val paint = textView.paint.apply {
textSize = textView.textSize
typeface = textView.typeface
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val staticLayout = StaticLayout.Builder
.obtain(text, 0, text.length, paint, availableWidth)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(0f, 1.0f)
.setIncludePad(textView.includeFontPadding)
.build()
staticLayout.lineCount
} else {
@Suppress("DEPRECATION")
val staticLayout = StaticLayout(
text,
paint,
availableWidth,
Layout.Alignment.ALIGN_NORMAL,
1.0f,
0f,
textView.includeFontPadding
)
staticLayout.lineCount
}
}
}
// 使用示例
TextViewUtils.processTextViewLineCount(
mTv, str, 300.dp2px()) { lineCount ->
if (lineCount > 1) {
//多行显示的逻辑
} else {
//单行显示的逻辑
}
}