Jetpack Compose : 超简单实现文本展开和收起

前言

文本展开和收起功能可以在需要显示较长文本或内容丰富的情况下,提供更好的用户体验和页面可读性,同时减少页面的冗余信息。可以说是发中常见的功能之一。

需要实现的点

  1. 文本超出最大显示行数时尾部显示"...展开"
  2. 点击"...展开"时显示全部文本并且尾部显示"...收起"

看看Text为我们提供了啥

查看Text的源码很容易看到overflow和maxLines两个属性。

Kotlin 复制代码
@Composable
fun Text(
    // 要显示的文本内容
    text: String,
    // Modifier修饰符
    modifier: Modifier = Modifier,
    // 文本内容溢出处理方式:Clip、Ellipsis、Visible
    overflow: TextOverflow = TextOverflow.Clip,
    // 最大文本行数
    maxLines: Int = Int.MAX_VALUE,
    // 计算新文本布局时执行的回调。
    onTextLayout: (TextLayoutResult) -> Unit = {},
    // 文本的样式配置
    style: TextStyle = LocalTextStyle.current
)

当我们的overflow设置为"Ellipsis"且文本超出最大行数时显示"..."。

因此我们的思路很简单,当文本控件显示"..."时在其尾部盖上"...展开",伪代码如下:

Kotlin 复制代码
Box {
	Text()
	Text(
		text = "...展开",
		modifier = Modifier
                .graphicsLayer {
                    translationX = ellipsisLeft
                    translationY = ellipsisBottom - size.height
                }
	)
}

如何获得文本内容溢出后的位置

还好Text控件提供了onTextLayout回调返回TextLayoutResult,这里贴对我们有用的几个方法:

Kotlin 复制代码
class TextLayoutResult constructor(

    val layoutInput: TextLayoutInput,

    val multiParagraph: MultiParagraph,

    val size: IntSize
) {

    val lineCount: Int get() = multiParagraph.lineCount

    fun isLineEllipsized(lineIndex: Int): Boolean = multiParagraph.isLineEllipsized(lineIndex)

    fun getLineTop(lineIndex: Int): Float = multiParagraph.getLineTop(lineIndex)

    fun getLineBottom(lineIndex: Int): Float = multiParagraph.getLineBottom(lineIndex)

    /**
     *  获取指定行右侧X坐标
     */
    fun getLineRight(lineIndex: Int): Float = multiParagraph.getLineRight(lineIndex)

    /**
     *	获取指定位置字符的水平偏移量
     */
    fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float =
        multiParagraph.getHorizontalPosition(offset, usePrimaryDirection)

    /**
     *  获取指定偏移量最接近的字符位置。
     */
    fun getOffsetForPosition(position: Offset): Int =
        multiParagraph.getOffsetForPosition(position)

}

ellipsisBottom可以通过getLineBottom直接获取取。

ellipsisLeft就相对要麻烦些,我们先要获取指定行右侧X坐标再减去ellipsisText的width。

先计算下ellipsisText的width,好在compose为我们提供非常便捷的api这里直接贴代码:

Kotlin 复制代码
val ellipsisMeasure = rememberTextMeasurer()
val ellipsisLayoutResult = ellipsisMeasure.measure(
    text = ellipsisText,
    style = style
)
val ellipsisWidth = ellipsisLayoutResult.size.width

但是直接使用会出现文字部分被覆盖的情况,所以我们通过该值先获取指定偏移量最接近的字符位置,再通过获取指定位置字符的水平偏移量得到真正的值。

Kotlin 复制代码
val ellipsisLeft = it.getHorizontalPosition(
       it.getOffsetForPosition(
           Offset(
                it.getLineRight(it.lineCount - 1) - ellipsisWidth,
                it.getLineTop(it.lineCount - 1)
           )
       ), true
)

完整代码

Kotlin 复制代码
@Composable
fun EllipsisText(
    text: AnnotatedString,
    color: Color,
    backgroundColor: Color,
    fontSize: TextUnit = TextUnit.Unspecified,
    lineHeight: TextUnit = TextUnit.Unspecified,
    maxLines: Int = Int.MAX_VALUE,
    inlineContent: Map<String, InlineTextContent> = mapOf(),
    onTextLayout: (TextLayoutResult) -> Unit = {},
    ellipsisText: String = "...全文",
    ellipsisColor: Color = colorResource(R.color.blue),
    onClick: () -> Unit = {},
    onEllipsisClick: () -> Unit = {},
) {
    val style = TextStyle.Default.copy(color = color, fontSize = fontSize)
    var ellipsisBottom by remember { mutableFloatStateOf(0f) }
    var ellipsisLeft by remember { mutableFloatStateOf(0f) }
    val ellipsisMeasure = rememberTextMeasurer()
    val ellipsisLayoutResult = ellipsisMeasure.measure(
        text = ellipsisText,
        style = style
    )
    val ellipsisWidth = ellipsisLayoutResult.size.width
    Box(modifier = Modifier.animateContentSize()) {
        Text(
            text = text,
            modifier = Modifier
                .clickable(
                    onClick = onClick,
                    indication = null,
                    interactionSource = remember { MutableInteractionSource() }
                )
                .background(backgroundColor),
            lineHeight = lineHeight,
            overflow = TextOverflow.Ellipsis,
            maxLines = maxLines,
            inlineContent = inlineContent,
            onTextLayout = {
                val offset = if (maxLines == Int.MAX_VALUE) 0 else ellipsisWidth
                ellipsisBottom = it.getLineBottom(it.lineCount - 1)
                ellipsisLeft = it.getHorizontalPosition(
                    it.getOffsetForPosition(
                        Offset(
                            it.getLineRight(it.lineCount - 1) - offset,
                            it.getLineTop(it.lineCount - 1)
                        )
                    ), true
                )
                if (ellipsisLeft + ellipsisWidth > it.size.width) {
                    ellipsisLeft = it.getHorizontalPosition(
                        it.getOffsetForPosition(
                            Offset(
                                (it.size.width - ellipsisWidth).toFloat(),
                                it.getLineTop(it.lineCount - 1)
                            )
                        ), true
                    )
                }
                onTextLayout(it)
            },
            style = style
        )
        Text(
            text = "$ellipsisText ",
            modifier = Modifier
                .graphicsLayer {
                    translationX = ellipsisLeft
                    translationY = ellipsisBottom - size.height
                }
                .clickable { onEllipsisClick() }
                .background(backgroundColor),
            style = style.copy(color = ellipsisColor)
        )
    }
}

如何使用

Kotlin 复制代码
var ellipsis by remember { mutableStateOf(false) }
var expand by remember { mutableStateOf(false) }
EllipsisText(
    text = buildAnnotatedString {
        append(
            "I am happy to join with you today in what will go down in history as the greatest demonstration for freedom in the history of our nation."
        )
    },
    color = colorResource(R.color.text_333),
    backgroundColor = colorResource(R.color.background),
    fontSize = 14.sp,
    maxLines = if (expand) Int.MAX_VALUE else 2,
    onTextLayout = {
        ellipsis = it.isLineEllipsized(it.lineCount - 1)
    },
    ellipsisText = if (expand) {
        "...收起"
    } else if (ellipsis) {
        "...展开"
    } else {
        ""
    }
) {
    expand = !expand
}

Thanks

以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。

如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧,您的鼓励是我前进的动力。

谢谢~~

源代码地址

推荐阅读

相关推荐
顾林海28 分钟前
Android ClassLoader加载机制详解
android·面试·源码
用户20187928316728 分钟前
🎨 童话:Android画布王国的奇妙冒险
android
whysqwhw1 小时前
OkHttp框架的全面深入架构分析
android
你过来啊你1 小时前
Android App冷启动流程详解
android
泓博2 小时前
KMP(Kotlin Multiplatform)改造(Android/iOS)老项目
android·ios·kotlin
移动开发者1号2 小时前
使用Baseline Profile提升Android应用启动速度的终极指南
android·kotlin
移动开发者1号2 小时前
解析 Android Doze 模式与唤醒对齐
android·kotlin
Devil枫4 小时前
Kotlin扩展函数与属性
开发语言·python·kotlin
菠萝加点糖4 小时前
Kotlin Data包含ByteArray类型
android·开发语言·kotlin
IAM四十二9 天前
Google 端侧 AI 框架 LiteRT 初探
android·深度学习·tensorflow