写点什么

Compose 把 Text 组件玩出新高度

作者:Halifax
  • 2022-12-11
    江苏
  • 本文字数:10721 字

    阅读完需:约 35 分钟

本文首发于稀土掘金 https://juejin.cn/post/7140529542665338910



一、前言

开始前,建议大家可以去先看一下我们的这一篇文章Compose挑灯夜看 - 照亮手机屏幕里面的书本内容,对阅读本篇文章有益。


我不知道有多少人用过“纯纯写作”,今天想起来,就以它为开头引子,纯纯写作里面有一个下面这样的功能,如下图:



当然这个功能只是我们今天这个文章里面提到的Compose的Text花样玩法其中之一,且看我们下面一一道来。

二、Text 组件介绍

耐心往下看,切记心浮气躁,我们先介绍一下 Text 组件,如果觉得简单,可以跳过目录二,如果想看官方的 Text 文档,点击这里


// androidx.compose.material.Text@Composablefun Text(    // 要显示的文本内容    text: String,    // Modifier修饰符    modifier: Modifier = Modifier,    // 文本颜色    color: Color = Color.Unspecified,    ......    // 在文本内容上面绘制的装饰(如,下划线)    textDecoration: TextDecoration? = null,    // 行高    lineHeight: TextUnit = TextUnit.Unspecified,    // 文本内容溢出处理方式:Clip、Ellipsis、Visible    overflow: TextOverflow = TextOverflow.Clip,    // 是否处理换行符    softWrap: Boolean = true,    // 计算新文本布局时执行的回调。    // [TextLayoutResult] 包含段落信息、大小    // 文本、基线等。回调可用于添加额外的装饰和    // 文本的功能。例如,围绕文本绘制选择。    onTextLayout: (TextLayoutResult) -> Unit = {},    // 文本的样式配置    style: TextStyle = LocalTextStyle.current)
复制代码


我们精简了Text组件里面提供的参数,参数含义见上面的注释。


我们平常修改一下:“文字大小、字体颜色、字体、Modifier 修饰符”,感觉就差不多了,但事情并不往往那么简单。


比如:我们这一篇文章中Compose挑灯夜看 - 照亮手机屏幕里面的书本内容,还用到了 TextStyle 里面的 brush 的 API。


看了Text源码,它提供的方法我们知道:


显示文字的最基本方法是使用以 String 作为参数的 Text 可组合项。

同一 Text 可组合项中设置不同的样式,必须使用 AnnotatedString。


如果只是基于基础参数使用的话,很多功能,都会止步于此,如果要实现一些更复杂的效果话,这个时候就需要通过onTextLayout的回调来定制了。


我们也可以通过TextMeasurer可以轻松的实现BasicText一样的功能,我们不再需要nativeCanvas辛苦的绘制Text,这个在文章后面会有讲解。


// TextMeasurer简单示例,文章后面会有介绍,比如:目录五,会用到它。val text = buildAnnotatedString { append("我们不会期待米粉的期待") }val textMeasure = rememberTextMeasurer()val textLayoutResult = textMeasure.measure(text = text, style = TextStyle(color = Color.Black, fontSize = 18.sp))Box(modifier = Modifier.fillMaxSize().systemBarsPadding()) {    Canvas(modifier = Modifier.fillMaxWidth()) {       drawText(textLayoutResult = textLayoutResult)     }}
复制代码


我们看Text组件的 onTextLayout 给我们回调了TextLayoutResult,我们看看这个类里面给我们提供了什么:


// androidx.compose.ui.text.TextLayoutResultclass TextLayoutResult constructor(    // 保存文本布局计算参数集的数据类。    val layoutInput: TextLayoutInput,    // 文本布局计算返回的多段落object    val multiParagraph: MultiParagraph,    // 文本内容占的宽度和高度    val size: IntSize) {    ......    // 返回指定字符偏移的字符边界    fun getBoundingBox(offset: Int): Rect = ...    // 返回包含指定文本范围的路径    fun getPathForRange(start: Int, end: Int): Path = ...    // 返回指定行的顶部坐标    fun getLineTop(lineIndex: Int): Float = ...    // 返回指定行的左边水平x坐标    fun getLineLeft(lineIndex: Int): Float = ...    // 返回指定行的右边水平x坐标    fun getLineRight(lineIndex: Int): Float = ...    // 返回指定行的底部坐标    fun getLineBottom(lineIndex: Int): Float = ...    // 获取指定文本偏移的水平位置。返回与文本起始偏移量的相对距离    fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float = ...    ......}
复制代码


TextLayoutResult 给我们提供太多有用的东西了,更多参数和方法留给读者自己去阅读,不可能一篇文章全部介绍完,篇幅有限,关键是我们这篇主讲的是“玩些新花样”,我们下面开始玩些新花样

三、绘制自定义文本跨行

1、文本跨行背景绘制

回到文章开头的地方,我们提到了“纯纯写作”这个引子,如何实现文本跨行背景绘制呢?

1、getBoundingBox 方式

首先我们可能会想到,获取到单个文字的left.xtop.y,我们能从TextLayoutResult哪个方法里面获取呢?


TextLayoutResult里面有getBoundingBox这个方法,可以获取“指定字符偏移的字符边界”,我们这样是不是就可以获取对应的文字在哪个位置了,对不对?


由于需要在文本后面绘制背景色,那肯定需要 Modifier drawBehind修饰符:


// 举个例子var onDraw: DrawScope.() -> Unit by remember { mutableStateOf({}) }Text(   text = text,   style = MaterialTheme.typography.body1.copy(lineHeight = 20.sp),   modifier = Modifier.drawBehind { onDraw() },   onTextLayout = { layoutResult ->        // 随便测试一段text里面的文本内容        val findIndex = text.indexOf("周")        onDraw = {            val boundsRect = layoutResult.getBoundingBox(findIndex)            drawRect(                brush = SolidColor(Color(0xFFFF6E00)),                topLeft = boundsRect.topLeft,                size = boundsRect.size            )       }   })
复制代码



我们可以看到,取到了单个字符串所在的位置,并成功绘制了背景色,那么我们如果绘制跨行背景的话,是不是循环去获取getBoundingBox对应的值呢?


// 注意:这段代码没有问题Text(   ...   onTextLayout = { layoutResult ->        // 随便测试一段text里面的文本内容        val findIndex = text.indexOf("周末七国分争,并入于秦。")        val endIndex = findIndex.plus("周末七国分争,并入于秦。".length)        onDraw = {            for (index in findIndex until endIndex) {               // 循环获取单个字符串的Rect边界               val boundsRect = layoutResult.getBoundingBox(index)               drawRect(                     brush = SolidColor(Color(0xFF899BBE)),                     topLeft = boundsRect.topLeft,                     size = boundsRect.size                )             }          }   })
复制代码



我们可以看到,这里没有成功跨行绘制,我们发现只要到了一行的最后一个字符串,它的值就变成了下面这样:


Rect.fromLTRB(1008.0, 0.0, 0.0, 64.0)
复制代码


我发现Issue Tracker里面也有人发过这个问题,谷歌修复的时间是“7 月 29 号”,感兴趣的可以点击查看修改的内容


而我写这个示例的compose版本是1.2.1,谷歌并没有把这个代码合并到1.2.1里面,我在1.3.0-alpha03里面找到了合并记录。


于是,上面的代码,只要升级到了 compose 1.3.0-alpha03+ 的版本上,就可以正常绘制了:



2、getPathForRange 方式

我们也可以通过 drawPath 来绘制跨行文本背景,我们在TextLayoutResult里面发现getPathForRange可以返回包含指定文本范围的路径


val findIndex = text.indexOf("....")val path = layoutResult.getPathForRange(findIndex,"....".length)onDraw = {    drawPath(         path = path,         brush = SolidColor(Color(0xFF899BBE))    )}
复制代码


同样可以实现上面的效果,由于这里不是一行一行的去绘制,所以这里不能给 path 添加圆角,如果在这里添加圆角会出现下面这样的效果:



如果它只有一行,那没有问题,直接path.addRoundRect就行了。


这里再插一句,建议读者在 drawPath 里面添加一行下面这段,试试效果:


style = Stroke(width = 1.dp.toPx())
复制代码


多行文本我们想实现跨多行文本圆角背景选中,如何实现呢?下面请看:扩展getBoundingBox实现

3、扩展 getBoundingBox 实现

先看个效果



我们看上面这个效果,如果你看完上面的文章内容,看到这里,应该知道,我们这里需要拆解“”,每行都需要单独绘制遍历每一行,然后读取它们的边界,目前源码里面并没有这个方法,那么我们就自己增加一个扩展


我们再来回顾一下,TextLayoutResult里面的方法:


// androidx.compose.ui.text.TextLayoutResultclass TextLayoutResult constructor(...) {    ...    // 返回指定行的顶部坐标    fun getLineTop(lineIndex: Int): Float = ...    // 返回指定行的左边水平x坐标    fun getLineLeft(lineIndex: Int): Float = ...    // 返回指定行的右边水平x坐标    fun getLineRight(lineIndex: Int): Float = ...    // 返回指定行的底部坐标    fun getLineBottom(lineIndex: Int): Float = ...    // 获取指定文本偏移的水平位置。返回与文本起始偏移量的相对距离    fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float = ...    ...}
复制代码


获取指定某段文本占的开始行结束行


val startIndex = text.indexOf("指定的文本内容")val endIndex = start.plus("指定的文本内容".length)
// 开始的行val startLine = getLineForOffset(startIndex)// 结束的行val endLine = getLineForOffset(endIndex)
复制代码


知道从哪一行开始,哪一行结束,这个时候需要一个 for 循环了,那么循环每行,如何知道这一行的指定内容所在的坐标位置呢?


指定行的 Top 位置: TextLayoutResult#getLineTop


指定行的 Bottom 位置: TextLayoutResult#getLineBottom


我们需要注意,左侧和右侧的位置:


// com.melody.text.effect.components.TextLayoutExtension.kt
// 左侧:if (indexLine == startLine) { // 获取指定文本偏移的水平位置。返回与文本起始偏移量的相对距离 getHorizontalPosition(offset = start, usePrimaryDirection = true)} else { // 返回指定行的左边水平x坐标 getLineLeft(indexLine)}
// 右侧:if (indexLine == endLine) { // 获取指定文本偏移的水平位置。返回与文本起始偏移量的相对距离 getHorizontalPosition(offset = end, usePrimaryDirection = true)} else { // 返回指定行的右边水平x坐标 getLineRight(indexLine)}
复制代码


如果是首行尾行,需要通过TextLayoutResult#getHorizontalPosition 获取当前左侧或者右侧与文本起始偏移量的相对距离。


循环首行尾行,记录下,所有的Rect,接下来,我们只需要遍历这个列表,然后通过drawPath去绘制即可。


val findIndex = text.indexOf("指定的某段文字内容")
// 注意:getBoundingBoxRectList方法里面我们区分了:“单行”和“多行”2个分支!!!val rectList = layoutResult.getBoundingBoxRectList(findIndex,findIndex.plus("指定的某段文字内容".length))onDraw = { rectList.forEachIndexed { index, rect -> // 清除路径中的所有直线和曲线,保留内部数据结构便于更快地重用 path.asAndroidPath().rewind() // 具体值设置,可以在文章末尾查看,我们提供的源码地址。 // 我们可以在这里,增加边距,防止挨的太紧凑。 path.addRoundRect(RoundRect(....)) // 绘制背景 drawPath( path = path, brush = SolidColor(Color(0xFF276FFF).copy(alpha = 0.3F)), style = Fill ) // 绘制边框 drawPath( path = path, brush = SolidColor(Color(0xFF276FFF)), style = Stroke(width = 1.sp.toPx()) ) }}
复制代码



2、内容下方添加波浪线动画

拆解任务:我们需要获取到指定内容,再给它绘制一个波浪线,最后再加上动画。


不知道怎么画波浪线,我们先画个直线


val rectList = layoutResult.getBoundingBoxRectList(...)
onDraw = { rectList.forEach { rect-> val underline = rect.copy(top = rect.bottom - 2.sp.toPx()) drawRect( color = Color.Blue, topLeft = underline.topLeft, size = underline.size, ) }}
复制代码



波浪线:需要用Path来画,画出来需要它能动,就需要Animation


我们定义一个buildWaveLinePath创建波浪线Path:


val TWO_PI = 2 * Math.PI.toFloat()
private fun Path.buildWaveLinePath(bound: Rect, waveLength:Float, animProgress: Float): Path { asAndroidPath().rewind() //moveTo(bound.left, bound.height) // 不能放这里 var pointX = bound.left while (pointX < bound.right) { val offsetY = bound.bottom + sin(((x - bound.left) / waveLength) * TWO_PI + (TWO_PI * animProgress)) if(x == bound.left) { moveTo(bound.left, offsetY) } lineTo(x, offsetY) pointX += 1F } return this}
复制代码


如果要增加 波浪线幅度 sin(x) * n


// 像这样:sin(((x - bound.left) / waveLength) * TWO_PI + (TWO_PI * animProgress)) * 5
复制代码


然后定义一个rememberInfiniteTransition()无限运行的动画,去更新animProgress,需要注意一点DrawScope#drawPath 绘制波浪线,一定要设置PathEffect否则你的波浪线会出现小山丘那种浪线:


val pathStyle = Stroke(    ...    pathEffect = PathEffect.cornerPathEffect(radius = 9.dp.toPx()))drawPath(    path = path,    ...    style = pathStyle)
复制代码



四、分离文字动画

我们上面一直在讲的都是绘制背景相关,并没有提到对文字做什么动画之类的,这个目录,我们要对文字内容,开刀。


我们看一下要实现的效果(gif有点卡):



这里我们不能ModifierdrawBehind修饰符了,实验一,这里我们需要用drawWithCache,我们再看一眼,drawBehind


// androidx.compose.ui.drawfun onDrawBehind(block: DrawScope.() -> Unit): DrawResult = onDrawWithContent {    block()    drawContent()}
复制代码


block()就是后面绘制的背景而已,drawContent()是上层的内容,我们这里直接用drawWithCache,但我们不调用drawContent(),分离文字的时候就不会发生内容重叠绘制。


// 像这样的,不调用drawContent()Modifier.drawWithCache {    onDrawWithContent {        onDraw()    }}
复制代码


同样的我们通过 onTextLayout 获取 TextLayoutResult


从文章开头看到这里的同学肯定知道了,我们这里需要用TextLayoutResult#getBoundingBox获取每个内容的位置。


然后再给它通过drawText画上去,看上去有点麻烦的样子,这分离后能和原来的一样吗?


注意:这个不是nativeCanvas#drawText


我们看一下DrawScope#drawText需要我们传什么?


// 方法一fun DrawScope.drawText(    textMeasurer: TextMeasurer,    text: String,    topLeft: Offset = Offset.Zero,    style: TextStyle = TextStyle.Default,    overflow: TextOverflow = TextOverflow.Clip,    softWrap: Boolean = true,    maxLines: Int = Int.MAX_VALUE,    size: IntSize = IntSize(        width = ceil(this.size.width).roundToInt(),        height = ceil(this.size.height).roundToInt()    ))
// 方法二fun DrawScope.drawText( textLayoutResult: TextLayoutResult, color: Color = Color.Unspecified, topLeft: Offset = Offset.Zero, alpha: Float = Float.NaN, shadow: Shadow? = null, textDecoration: TextDecoration? = null)
复制代码


我们可以看到,方法一 的第一个参数是TextMeasurer,这个API可实现任意文本布局计算,创建与 BasicText 相同的结果,


如果想修改 densitylayoutDirectionfontFamilyResolver,就需要把Compose版本升级到了1.3.0-beata01+,然后可以使用TextMeasurer去构造里面的自定义值:


val textMeasurer by remember { TextMeasurer(...) }
复制代码


我们看一下TextMeasurer的部分源码解释:


// androidx.compose.ui.text
class TextMeasurer constructor( // 用于加载到TextStyle 和 SpanStyles 中给出的字体 private val fallbackFontFamilyResolver: FontFamily.Resolver, // 屏幕的密度 private val fallbackDensity: Density, // 布局方向,当前是LTR还是RTL,阿拉伯这些国家用的就是RTL,从右到左显示 private val fallbackLayoutDirection: LayoutDirection, // TextMeasurer 内部缓存的容量 private val cacheSize: Int = DefaultCacheSize) { ... // [TextLayoutResult] 包含段落信息、大小 // 文本、基线等。可用于添加额外的装饰和 // 文本的功能。例如,围绕文本绘制选择等。 fun measure(...): TextLayoutResult { ... } ...}
复制代码


回到上面,我们看DrawScope#drawText方法二,需要传TextLayoutResult,这里就需要我们使用TextMeasurer#measure


我们实验一的效果,只需要DrawScope#drawText方法一


我们挨个取字符串,通过TextLayoutResult#getBoundingBox获取对应的字符串的位置


for (index in text.indices){    val rect = it.getBoundingBox(index)    drawText(        textMeasurer = ...,        text = text[index].toString(),        topLeft = rect.topLeft,    )}
复制代码



如何让它动起来呢?我们需要用到DrawScope#withTransform


withTransform({    rotate(       degrees = ...,       pivot = rect.center     )})
复制代码


这样是不是就可以动起来了(GIF录屏卡):



那么如何增加整个文字颜色渐变呢?如果你看过Compose挑灯夜看 - 照亮手机屏幕里面的书本内容这一篇文章就知道怎么做了,关键代码如下


val brush = Brush.horizontalGradient(    listOf(        Color(0xFF22B6FF),        Color(0xFFB732FF),        Color(0xFFFF1D37)    ))
// graphicsLayer(alpha = 0.99F)是合成的关键,为什么小于1F,读者可以打开// graphicsLayer里面的alpha注释:小于1.0F的alpha值会将其内容隐式裁剪到其边界// 如果太小就会出现太透明,所以我们这里用0.99F就行了Modifier.graphicsLayer(alpha = 0.99F).drawWithCache { onDrawWithContent { onDraw() drawRect(brush, blendMode = BlendMode.SrcAtop) }}
复制代码



到这里,我觉得还可以再修改一下,我们是否可以通过其他方式去,单个文字去绘制做动画呢?


我们可以通过clipRect设置显示区域:


val boundingBoxList = textLayoutResult.layoutInput.text.indices.map {    textLayoutResult.getBoundingBox(it)}for (index in textLayoutResult.layoutInput.text.indices) {    val box = boundingBoxList[index]    withTransform({        rotate(...)        // 设置显示区域        clipRect(box.left, box.top, box.right, box.bottom)    }) {        drawText(textLayoutResult)    }}
复制代码


我们一样可以实现上面的效果。


延伸:是否可以配合PathMeasure来做更多有意思的效果呢?😍当然可以啦,篇幅有限,有兴趣的可以去尝试一下。

五、展开/收起更多文本

刚接触的同学,可能不知道怎么去实现这种效果,或者说实现完,发现总达不到就是那种,能拿的出手的那种效果。



这种设计,移动到都会有的,所以我们这里拿出来,并讲解其中的思路,源码会在文章末尾提供出来,先看一下,最终的效果:



我们可以看到其中的细节触摸到“展开”和“收起”文本内容的时候,文字背景色会有选中的效果,同时我们可以看到“省略号”是和正文在一起的,同时,我们也处理了:内容换行符+展开内容的功能。


大家可以先不急着往下看,可以自己去尝试写一下,写不出来,没有思路的话,再来往下看。


短短的这些描述,我们能捕捉到下面几点


  1. 手势点击+触摸。

  2. 展开/收起,选中高亮。

  3. 展开/收起,正文内容展开有动画过渡。

  4. 按钮位置偏移量计算。

  5. 如何处理“展开”,“收起”按钮的摆放。

一、手势点击和触摸:

我们使用手势事件作用域 awaitPointerEventScope来处理 DOWN/MOVE/UP 事件回调。


internal suspend fun PointerInputScope.detectTouchGestures(    onDown: ((Offset) -> Unit),    onMove: ((Offset) -> Unit),    onUp: (Offset) -> Unit): Unit = coroutineScope {    launch {        forEachGesture {            awaitPointerEventScope {                val down = awaitFirstDown().also { it.consume() }                // 这里处理:ACTION_DOWN                ....                while (true) {                    val event: PointerEvent = awaitPointerEvent()                    if (event.changes.fastAny { it.pressed }) {                        ....                        // 这里处理:ACTION_MOVE                    } else {                        // 这里处理:ACTION_UP                        break                    }                }            }        }    }}
复制代码

二、文字背景内容选中高亮:

我们通过上面的detectTouchGestures回调的Offset,得到当前手势触摸的位置,这个Offset是通过PointerInputChange获取到的。


我们知道触摸的位置,还不够,我们还要知道“展开”和“收起”按钮,当前的位置,如果知道呢,别急,我们在后面再说,这里先跳过。


高亮的话,就是需要,触摸的位置按钮的位置进行对比,我们可以稍微增大,按钮的可触摸点击的范围,可以提供如下方法来判断是否在按钮上触摸移动:


private fun isMoveSeeMore(touchOffset: Offset, textLayoutSize: IntSize, btnOffset: Offset): Boolean {    val touchSizeWidth = textLayoutSize.width    val touchSizeHeight = textLayoutSize.height    return touchOffset.x >= btnOffset.x - touchSizeWidth            && touchOffset.x <= btnOffset.x + touchSizeWidth            && touchOffset.y >= btnOffset.y - touchSizeHeight            && touchOffset.y <= btnOffset.y + touchSizeHeight}
复制代码


如果为true,就表示当前是在“展开”和“收起”按钮上触摸,可以修改背景,添加高亮背景。

三、正文内容展开过渡动画:

我们这里,又需要用到Animatable来触发动画更新了,我们通过Animatable#animateTo更新。


正文内容展开过渡,我们这里就需要用到clipRect裁剪内容到可见范围内,同时需要更新当前组件的高度Modifier.height,保证组件空间正常缩放,2 者缺一不可。


clipRect示例如下:


withTransform({    clipRect(        left = ...,        top = ...,        ....    )}) {   drawText(textLayoutResult)}
复制代码


展开内容的高度示例:


val expandHeight by remember {    derivedStateOf {        if (!isInitRender && animaTable.value > 0) {            animaTable.value + if (expanded) seeMoreTextLayoutResult.size.height else 0        } else {            collapseMinHeight        }    }}
// 高度随着我们动画,而更新Modifier.height(expandHeight)
复制代码

四、按钮偏移位置:

怎么计算偏移位置呢?如果你从文章上面看到这里,我想你应该知道,怎么计算,估计这里会有读者说作者这人二臂吧,我怎么会知道呢?🤣


正文默认折叠的内容,假如只显示“3行”,那么我们就需要获取第3行内容的topbottom位置。


假如是第3行,我们先获取“省略号”的位置,我们可以通过textLayoutResult.getLineRight(2)获取右侧的位置,这个时候还不能直接使用,我们这里还需要处理:


  1. textLayoutResult.getLineRight(2)是否小于或者大于,正文内容的宽度 - (省略号+展开/收起的内容宽度和)。

  2. 正文内容渲染一半有换行符的情况。


拿到上面,省略号的位置之后,我们就可以获取“展开”/“收起”的位置了:


seeMoreOffset = seeMoreOffset.copy(    x = if (expanded) 0F else ellipsisLineLeftX + ellipsisTextLayoutResult.size.width,    y = (expandHeight - seeMoreTextLayoutResult.size.height))
复制代码


省略号,“展开”/“收起”的文字的宽度和高度,我们这里需要通过TextMeasurermeasure去测量,拿到TextLayoutResult就可以获取文本的宽度和高度了。


// 查看更多val seeMoreTextLayoutResult = seeMoreTextMeasure.measure(    text = readOnlyMeasureText,    style = textStyle)
// 测量省略号,返回TextLayoutResultval ellipsisTextLayoutResult = seeMoreTextMeasure.measure( text = ellipsisText, style = textStyle)
复制代码

五、如何处理“展开”,“收起”按钮的摆放

我们上面提到了,需要通过clipRect设置可见区域,我们这里需要先裁剪省略号那一行的上一行。


我们需要,把内容拆分一下,先绘制不含省略号的那一行,再绘制含省略号的那一行。



withTransform({ clipRect(...)}) { // 绘制正文,不包含最后一行文本内容 drawText(textLayoutResult)}withTransform({ clipRect(...)}) { // 绘制正文,最后一行内容,不包含:省略号 drawText(textLayoutResult)}
复制代码


这个时候,我们需要进行“省略号”和“展开,收起”按钮进行绘制了,第四点,我们提到了按钮偏移位置,然后我们只需要通过drawText,然后传入topLeft进行摆放即可。


注意:这里我们需要判断,展开的最大高度和默认折叠的高度对比,保证默认折叠的高度>=展开最大高度的时候,不显示“省略号”和“展开,收起”按钮


if (!expanded) {    // 绘制省略号    // 默认收起状态,才显示... 省略号,省略号和正文一个颜色,一个字体大小,和:展开、收起样式不同    drawText(        textLayoutResult = ellipsisTextLayoutResult,        topLeft = Offset(...)    )}// 绘制:”展开“ 或者 ”收起“drawText(    textMeasurer = seeMoreTextMeasure,    text = if (expanded) collapseStateText else expandStateText,    style = ..., // 文字大小,颜色,单独设置    topLeft = seeMoreOffset)
复制代码

六、全文涉及的示例源码

本文所介绍的内容全量源码在这里:ComposeTextEffect



看完记得 😘😘 点赞❤️ + 评论❤️ + 关注❤️


发布于: 刚刚阅读数: 3
用户头像

Halifax

关注

Android 2021-09-13 加入

星光不问赶路人,岁月不负有心人

评论

发布
暂无评论
Compose把Text组件玩出新高度_android_Halifax_InfoQ写作社区