D10/ 我要怎麽把文字变美美的 - Text & AnnotatedString

今天大概会聊到的范围

  • AnnotatedString
  • Text

在 Compose 中显示文字时,我们可以使用 Text 这个 Composable。但没有任何一个文字是单独存在的,所有的文字都会搭配着字型、大小、颜色来显示。

让我们来看看 Text 的 function 定义:

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
)

Text 这个 Composable 是一个参数很多的 Composable ,text 和 modifier 算是最好理解的,另外很多参数可以用来调整、美化这个文字。

刚刚提到的字型、大小及颜色,分别就是 fontFamily 代表字型,color,代表文字颜色。fontSize 就是文字大小。

除了这些之外,fontStyle 就是常见的斜体,可以透过 fontStyle = FontStyle.Italic 设定。

那粗体呢?粗体可以透过 fontWeight 设定。fontWeight = FontWeight.Bold 可以设定成常见的粗体。但 fontWeight 还有不同的级距,从 FontWeight.Thin ~ FontWeight.Black,同时也代表 FontWeight.W100 ~ FontWeight.W900,数字越大字越粗。

字元间距可以透过 letterSpacing 设定,行高可以透过 lineHeight 设定。如果想要做删除线或底线,可以透过 textDecoration 进行装饰。

如果文字很长时,可以透过 overflow 设定当文字超出框时是要截断还是留三个点、透过 softWrap 决定是否要换行,最後透过 maxLines 设定换行後最多可以有几行。

如果有常用的文字格式,可以透过 TextStyle 将上述说到的各种参数设定好後,在透过 style 参数一次设定整个格式。要注意的是 style 与外部的其他参数同时设定时,会被外部的参数给覆写掉

Text(
    text = "AAAAA",
    color = Color.Green,
    style = TextStyle(
        color = Color.Red
    )
)

虽然 TextStyle 内有设定 color = Color.Red,但会被外部的 color = Color.Green 覆盖掉,显示绿色

AnnotatedString

刚刚其实刻意简化了 text 这个参数,text 除了可以接收 String 之外,还能接受 AnnotatedString

AnnotatedString 其实很类似原本 android.text 的 SpannableString。 可以逐字的调整文字的 style。

Text(
    text = AnnotatedString(
        "BBBBB",
        spanStyle = SpanStyle(
            color = Color.Green
        )
    )
)

这样 BBBBB 会显示为绿色

这样好像和直接设定颜色没什麽差异?那 AnnotatedString 的好处到底在哪里呢?

Text(
    text =
    AnnotatedString(
        "BBBBB",
        spanStyle = SpanStyle(
            color = Color.Green
        )
    ) + AnnotatedString(
        "BBBBB",
        spanStyle = SpanStyle(
            color = Color.Red,
            fontWeight = FontWeight.Black
        )
    )
)

https://ithelp.ithome.com.tw/upload/images/20210924/20141597sx51Aqnnqf.png

AnnotatedString 可以互相串接。同一个 Text component 内可以有多个不同的 style

Text(
    text = buildAnnotatedString {
        append(
            AnnotatedString("CCCCC", spanStyle = SpanStyle(Color.Red))
        )
        append(
            AnnotatedString("CCCCC", spanStyle = SpanStyle(Color.Green))
        )
        append(
            AnnotatedString("CCCCC", spanStyle = SpanStyle(Color.Blue))
        )
    }
)

https://ithelp.ithome.com.tw/upload/images/20210924/20141597QxcLviBJkO.png

也可以透过 builder 做到一样的事情,将不同的 AnnotatedString 加在一起。也可以 append 没有 style 的文字

Text(
    text = buildAnnotatedString {
        append(
            AnnotatedString("Red", spanStyle = SpanStyle(Color.Red))
        )
        append(" is not ")
        append(
            AnnotatedString("Blue", spanStyle = SpanStyle(Color.Blue))
        )
    }
)

https://ithelp.ithome.com.tw/upload/images/20210924/20141597WeuuAKS4gR.png

在同一个字串加入多个样式

一样是透过 builder,我们可以先设定一个字串,在对特定的位置设定特定的 style。Style 重复设定在一样的位置上。例如这边我想将第一个字和第二个字设定为不同颜色,但是两个字都要有底线:

Text(
    text = buildAnnotatedString {
        append("Multiple style in one text")
        addStyle(
            style = SpanStyle(
                color = Color.Red,
                fontWeight = FontWeight.Bold,
                fontStyle = FontStyle.Italic
            ),
            start = 0,
            end = 8
        )
        addStyle(
            style = SpanStyle(
                color = Color.Blue,
                fontFamily = FontFamily.Monospace,
            ),
            start = 9,
            end = 14
        )
        addStyle(
            style = SpanStyle(
                textDecoration = TextDecoration.Underline
            ),
            start = 0,
            end = 14
        )
    },
)

https://ithelp.ithome.com.tw/upload/images/20210924/201415979EsByvlO9G.png

SpannableString

在使用原本的 Android TextView 时,我们若要做到一样的动作,会需要使用 SpannableString。但是 SpannableString 之所以可以运作,是因为 TextView 的内部,会去判断目前的文字是否是 Spannable ,并且特殊处理。但在 Compose 中,Text 不会去判断 SpannableString,因此无法使用原先在 Android 专案中建立好的 SpannableString。

因此,在使用 Compose 时,会需要重新建构 AnnotatedString。但为了满足好奇,还是研究了一下如何将 SpannableString 转换成 AnnotatedString。

fun SpannableStringBuilder.toAnnotatedString(): AnnotatedString {
    // ...
}

我在 SpannableStringBuilder 上建立一个 toAnnotatedString 的 extension function,并预期这个 function 回传 AnnotatedString

data class SpanCollection(val start: Int, val end: Int, val spanStyles: List<CharacterStyle>)


fun SpannableStringBuilder.toAnnotatedString(): AnnotatedString {
    var idx = 0
    var next: Int
    
    val spanCollections = mutableListOf<SpanCollection>()
    
    while (idx < this.length) {
        // find the next span trasition
        next = nextSpanTransition(idx, this.length, CharacterStyle::class.java)
        
        // get all spans in this range
        val charStyleSpans = getSpans(idx, next, CharacterStyle::class.java)
        
        spanCollections.add(SpanCollection(idx, next, charStyleSpans.toList()))
        
        idx = next
    }

    // ... 
}

在开始组成 AnnotatedString 之前,先透过 while loop 将 SpannableStringBuilder 中的每个 SpanStyle 抓出来,并且将 style 与其影响的位置存放在一个 list 中。

fun SpannableStringBuilder.toAnnotatedString(): AnnotatedString {
    var idx = 0
    var next: Int
    
    val spanCollections = mutableListOf<SpanCollection>()
    
    
    while (idx < this.length) {
        // find the next span trasition
        next = nextSpanTransition(idx, this.length, CharacterStyle::class.java)
        
        // get all spans in this range
        val charStyleSpans = getSpans(idx, next, CharacterStyle::class.java)
        
        spanCollections.add(SpanCollection(idx, next, charStyleSpans.toList()))
        
        idx = next
    }
    
    
    return buildAnnotatedString {
        
        append([email protected]())
        
        spanCollections.forEach { spanCollection ->
            val (start, end, spans) = spanCollection
            
            spans.forEach {
                when (it) {
                    is ForegroundColorSpan -> {
                        val span = it
                        addStyle(
                            style = SpanStyle(color = Color(span.foregroundColor)),
                            start = start,
                            end = end
                        )
                    }
                    // .. other SpanStyle
                }
            }
            
        }
        
    }
}

最後,透过 builder 将 style 一一加入,建构 AnnotatedString。这边比较麻烦的是要将所有 SpanStyle 列举出来,并逐一转换成对应的 AnnotatedString 的 SpanStyle


Text 可能会是最常用的元件之一。虽然并不是所有的样式都会很常用到,但了解这个最基础元件的用法,也许会在有需要的时候有点帮助。


Reference:


<<:  Day 24 Google 演算法的影响

>>:  [Day_10]资料储存容器(3) - 字典(dict)

Day3 Pipeline 如何做版本控制 - NiFi Registry

前面已经大概介绍了一下 NiFi 的用途还有特性,那今天就来讲在 NiFi 中,其实是可以对一组 D...

Day.23 Binary Search Tree

终於讲到树,快接近尾声了(烟 二元搜寻图(Binary Search Tree)是一种很高效的资料结...

JavaScript Day 21. 陈述式 & 表达式

听说 JavaScript 的这两大类型「陈述式」与「表达式」也是很基本的观念,瞬间我感觉自己从来没...

RISC-V: 介绍

系列文章主要是为了练习规律产出,实验性质比较重一点, 内容会根据目前的反馈做出一点改变,欢迎感兴趣的...

08 | WordPress 分隔符号区块 Separator Block

若您想用行分隔两个区块的内容,分隔符号区块 正是您需要的工具。 若要新增分隔符号区块,请按一下区块插...