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

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

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

谢谢~~

源代码地址

推荐阅读

相关推荐
运维Z叔1 小时前
云安全 | AWS S3存储桶安全设计缺陷分析
android·网络·网络协议·tcp/ip·安全·云计算·aws
Reese_Cool3 小时前
【C语言二级考试】循环结构设计
android·java·c语言·开发语言
平凡シンプル3 小时前
安卓 uniapp跨端开发
android·uni-app
elina80133 小时前
安卓实现导入Excel文件
android·excel
严文文-Chris3 小时前
【设计模式-享元】
android·java·设计模式
趋势大仙4 小时前
SQLiteDatabase insert or replace数据不生效
android·数据库
DS小龙哥4 小时前
QT For Android开发-打开PPT文件
android·qt·powerpoint
试行5 小时前
Android实现自定义下拉列表绑定数据
android·java
Dingdangr9 小时前
Android中的Intent的作用
android
技术无疆9 小时前
快速开发与维护:探索 AndroidAnnotations
android·java·android studio·android-studio·androidx·代码注入