Android | 文本测量:从 Paint.measureText 到 StaticLayout 的替换

Paint.measureText() 测量时的一个隐藏Bug

有这么一个场景:需要根据文本内容动态调整布局,首先需要计算文本行,开始使用的是 Paint.measureText() 来计算行数,示例代码如下:

kotlin 复制代码
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 当作普通字符处理,而不是布局指令。这意味着:

kotlin 复制代码
val singleLine = "这是一行文本"
val multiLine = "这是第一行\n这是第二行"

measureText 会把 \n 也计算进了总宽度而没有特殊处理,比如上述multiLine本来是要展示两行的,但是通过paint.measureText测量完之后计算出来可能只有一行导致计算错误。

StaticLayout 来实现

为了解决上述问题,可以通过StaticLayout来测量, StaticLayout是 Android 专门用于文本布局的类,它会将 \n 视为换行指令。除此之外,StaticLayout还会考虑排版规则:包括对齐、间距、字体等,所以StaticLayout能提供精确布局信息:行数、每行高度、宽度等信息。以下是修改后的代码:

kotlin 复制代码
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是精确的行数,可以根据这个行数进行后续布局调整
}

封装成工具函数

为了方便使用,我们可以将上述逻辑封装成工具函数:

kotlin 复制代码
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 {
        //单行显示的逻辑
    }
}
相关推荐
_小马快跑_1 小时前
Android | Channel 与 Flow的异同点
android
树獭非懒3 小时前
告别繁琐多端开发:DivKit 带你玩转 Server-Driven UI!
android·前端·人工智能
三少爷的鞋4 小时前
为什么应该先在 IntelliJ 中学习 Kotlin 与协程,而不是直接上 Android Studio
android
不爱说话郭德纲19 小时前
告别漫长的HbuilderX云打包排队!uni-app x 安卓本地打包保姆级教程(附白屏、包体积过大排坑指南)
android·前端·uni-app
Sinclair1 天前
简单几步,安卓手机秒变服务器,安装 CMS 程序
android·服务器
雮尘1 天前
手把手带你玩转Android gRPC:一篇搞定原理、配置与客户端开发
android·前端·grpc
ktl1 天前
Android 编译加速/优化 80%:一个文件搞定,零侵入零配置
android
alexhilton2 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
冬奇Lab2 天前
InputManagerService:输入事件分发与ANR机制
android·源码阅读