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

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

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

谢谢~~

源代码地址

推荐阅读

相关推荐
Estar.Lee1 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯2 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey3 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!5 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟5 小时前
Android音频采集
android·音视频
小白也想学C7 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程7 小时前
初级数据结构——树
android·java·数据结构
闲暇部落9 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX11 小时前
Android 分区相关介绍
android
大白要努力!12 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle