You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose如何设置Text省略号显示在第9行中间位置

问题根源

Compose的Text组件maxLines参数仅支持整型传入,设置8.5这类小数值不会生效;同时自带的TextOverflow.Ellipsis逻辑固定将省略号渲染在指定最大行数的行尾,没有提供调整省略号位置的配置项,因此没法直接实现第9行中间截断、后缀跟操作文本的效果。

效果参考

  • 目标实现效果
    目标效果
  • 默认实现效果
    当前效果
实现方案

放弃组件自带的省略号逻辑,手动计算文本截断位置,拼接正文、省略号、可点击的操作文本即可,核心逻辑:

  • 通过onTextLayout回调拿到文本的布局信息,判断文本是否超出指定行数
  • 提前测量省略号+操作文本的总宽度,计算第9行剩余可用于渲染正文的宽度
  • 根据可用宽度算出第9行能容纳的最后一个正文偏移量,截断正文后拼接后缀
  • 用AnnotatedString给操作文本加单独样式和点击标记,实现点击展开/收起逻辑

可直接复用的实现代码:

@Composable
fun ExpandableText(
    text: String,
    seeMoreText: String = "See more",
    seeLessText: String = "See less",
    collapsedMaxLine: Int = 9,
) {
    var expanded by rememberSaveable { mutableStateOf(false) }
    val bodyStyle = MaterialTheme.typography.body1.copy(
        lineHeight = 20.sp,
        color = MaterialTheme.ts.colors.onBackgroundSecondary
    )
    val actionStyle = MaterialTheme.typography.body2.copy(
        color = MaterialTheme.ts.colors.actionColor
    )
    val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
    val suffixText = "... $seeMoreText"

    // 测量后缀文本总宽度,Compose 1.3+可用rememberTextMeasurer
    val textMeasurer = rememberTextMeasurer()
    val suffixWidth = remember(actionStyle, suffixText) {
        textMeasurer.measure(
            text = AnnotatedString(suffixText),
            style = actionStyle
        ).size.width
    }

    val displayText = remember(text, layoutResult.value, expanded, suffixWidth) {
        // 展开状态、布局未完成、文本行数不足时直接返回原文本
        if (expanded || layoutResult.value == null || layoutResult.value!!.lineCount <= collapsedMaxLine) {
            AnnotatedString(text)
        } else {
            val textLayout = layoutResult.value!!
            val lastLineIndex = collapsedMaxLine - 1
            // 计算第9行扣除后缀宽度后的可用正文宽度
            val lastLineRight = textLayout.getLineRight(lastLineIndex)
            val availableWidth = lastLineRight - suffixWidth
            // 拿到可用宽度对应的最后一个正文字符偏移
            val lastCharOffset = textLayout.getOffsetForPosition(
                Offset(
                    x = availableWidth,
                    y = textLayout.getLineTop(lastLineIndex) + textLayout.getLineHeight(lastLineIndex) / 2
                )
            )
            // 拼接文本,给操作按钮加样式和点击标记
            buildAnnotatedString {
                append(text.substring(0, lastCharOffset))
                append("... ")
                pushStringAnnotation(tag = "expand_toggle", annotation = "expand")
                withStyle(actionStyle.toSpanStyle()) {
                    append(seeMoreText)
                }
                pop()
            }
        }
    }

    ClickableText(
        modifier = Modifier.padding(top = 16.dp),
        text = displayText,
        style = bodyStyle,
        maxLines = if (expanded) Int.MAX_VALUE else collapsedMaxLine,
        onTextLayout = { layoutResult.value = it },
        onClick = { clickOffset ->
            displayText.getStringAnnotations(
                tag = "expand_toggle",
                start = clickOffset,
                end = clickOffset
            ).firstOrNull()?.let {
                expanded = true
            }
        }
    )

    // 展开状态下显示收起按钮
    if (expanded) {
        Text(
            modifier = Modifier
                .padding(top = 16.dp, bottom = 40.dp)
                .clickable { expanded = false },
            text = seeLessText,
            style = actionStyle
        )
    } else {
        Spacer(modifier = Modifier.height(40.dp))
    }
}

适配说明

  • 如果项目使用的Compose版本低于1.3,没有rememberTextMeasurer,可以添加一个隐藏的Text组件渲染后缀文本,通过onSizeChanged回调拿到宽度值即可
  • 如果正文包含富文本、内联图片、手动换行符,需要在计算截断偏移时额外判断,避免截断特殊字符导致显示异常
  • 调整collapsedMaxLine参数即可复用实现任意行数的中间截断效果,不需要修改核心逻辑

内容的提问来源于stack exchange,提问作者Android Dev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 05:00:49