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 {
        //单行显示的逻辑
    }
}
相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui