Jetpack Compose Text设置各类样式

4 阅读12分钟

Jetpack Compose Text设置各类样式

目录

  1. 基础概念与API介绍
  2. 文本样式设置详解
  3. 代码示例
  4. 与AnnotatedString的对比
  5. 性能优化与最佳实践

一、基础概念与API介绍

1.1 什么是buildSpannedString

buildSpannedString是Android平台提供的用于构建富文本(Spanned String)的DSL风格API。在Jetpack Compose中,虽然推荐使用AnnotatedString,但在某些场景下(特别是与Android原生View系统交互或需要特定Span类型时),buildSpannedString仍然是一个重要的工具。

// buildSpannedString位于androidx.core.text包中
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.core.text.bold
import androidx.core.text.color
import androidx.core.text.italic
import androidx.core.text.underline
import androidx.core.text.strikeThrough

1.2 核心API结构

buildSpannedString构建流程:
┌─────────────────────────────────────┐
│  1. 调用buildSpannedString { }       │
├─────────────────────────────────────┤
│  2.lambda中添加文本内容            │
├─────────────────────────────────────┤
│  3. 使用inSpans或便捷方法应用样式     │
├─────────────────────────────────────┤
│  4. 返回SpannableString实例          │
├─────────────────────────────────────┤
│  5. 转换为AnnotatedString或直接显示   │
└─────────────────────────────────────┘

1.3 基础使用模式

// 基础使用模式
val spannedText = buildSpannedString {
    // 普通文本
    append("普通文本 ")
    
    // 粗体文本
    bold { append("粗体文本 ") }
    
    // 彩色文本
    color(Color.Red) { append("红色文本") }
}

// 在Compose中使用
Text(text = spannedText.toAnnotatedString())

1.4 扩展函数概览

扩展函数作用对应Span类型
bold { }粗体样式StyleSpan(Typeface.BOLD)
italic { }斜体样式StyleSpan(Typeface.ITALIC)
underline { }下划线UnderlineSpan
strikeThrough { }删除线StrikethroughSpan
color(color) { }文本颜色ForegroundColorSpan
backgroundColor(color) { }背景颜色BackgroundColorSpan
scale(proportion) { }字体缩放RelativeSizeSpan
superscript { }上标SuperscriptSpan
subscript { }下标SubscriptSpan

二、文本样式设置详解

2.1 字体样式设置

2.1.1 粗体、斜体、正常样式
import androidx.core.text.bold
import androidx.core.text.italic
import androidx.core.text.inSpans
import android.text.style.StyleSpan
import android.graphics.Typeface

@Composable
fun FontStyleExample() {
    // 方式1:使用便捷扩展函数
    val text1 = buildSpannedString {
        append("这是")
        bold { append("粗体") }
        append("文本,")
        italic { append("斜体") }
        append("样式")
    }
    
    // 方式2:使用inSpans自定义组合
    val text2 = buildSpannedString {
        append("这是")
        inSpans(StyleSpan(Typeface.BOLD_ITALIC)) {
            append("粗斜体")
        }
        append("组合样式")
    }
    
    // 方式3:嵌套使用
    val text3 = buildSpannedString {
        bold {
            italic {
                append("粗体+斜体")
            }
        }
    }
    
    Column {
        Text(text = text1.toAnnotatedString())
        Text(text = text2.toAnnotatedString())
        Text(text = text3.toAnnotatedString())
    }
}
2.1.2 字体大小设置
import androidx.core.text.scale
import android.text.style.AbsoluteSizeSpan
import android.text.style.RelativeSizeSpan

@Composable
fun FontSizeExample() {
    // 相对大小(倍数)
    val relativeSizeText = buildSpannedString {
        append("正常大小 ")
        scale(1.5f) { append("1.5倍大") }
        append(" ")
        scale(0.8f) { append("0.8倍小") }
    }
    
    // 绝对大小(像素)
    val absoluteSizeText = buildSpannedString {
        append("默认大小 ")
        inSpans(AbsoluteSizeSpan(48, true)) {  // true表示使用dp单位
            append("48sp文字")
        }
    }
    
    Column {
        Text(text = relativeSizeText.toAnnotatedString())
        Text(text = absoluteSizeText.toAnnotatedString())
    }
}

2.2 文本颜色与背景

2.2.1 前景色与背景色
import androidx.core.text.color
import androidx.core.text.backgroundColor
import android.text.style.ForegroundColorSpan
import android.text.style.BackgroundColorSpan

@Composable
fun ColorExample() {
    val coloredText = buildSpannedString {
        // 前景色(文字颜色)
        color(Color.Red) {
            append("红色文字 ")
        }
        
        // 背景色
        backgroundColor(Color.Yellow) {
            append("黄色背景 ")
        }
        
        // 组合使用
        color(Color.White) {
            backgroundColor(Color.Blue) {
                append("白字蓝底")
            }
        }
    }
    
    Text(text = coloredText.toAnnotatedString())
}
2.2.2 渐变色文字(结合自定义Span)
import android.text.style.CharacterStyle
import android.text.TextPaint
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.Brush

// 自定义渐变色Span
class GradientSpan(private val colors: IntArray) : CharacterStyle() {
    override fun updateDrawState(tp: TextPaint?) {
        tp?.let {
            val width = it.measureText("渐变文字")
            val shader = android.graphics.LinearGradient(
                0f, 0f, width, 0f,
                colors,
                null,
                android.graphics.Shader.TileMode.CLAMP
            )
            it.shader = shader
        }
    }
}

@Composable
fun GradientTextExample() {
    val gradientText = buildSpannedString {
        append("普通文字 ")
        inSpans(
            GradientSpan(
                intArrayOf(
                    Color.Red.toArgb(),
                    Color.Blue.toArgb()
                )
            )
        ) {
            append("渐变文字")
        }
    }
    
    Text(text = gradientText.toAnnotatedString())
}

2.3 文本装饰效果

2.3.1 下划线与删除线
import androidx.core.text.underline
import androidx.core.text.strikeThrough
import android.text.style.UnderlineSpan
import android.text.style.StrikethroughSpan

@Composable
fun DecorationExample() {
    val decoratedText = buildSpannedString {
        // 下划线
        underline {
            append("下划线文本 ")
        }
        
        // 删除线
        strikeThrough {
            append("删除线文本 ")
        }
        
        // 组合装饰
        underline {
            strikeThrough {
                append("双装饰文本")
            }
        }
    }
    
    Text(text = decoratedText.toAnnotatedString())
}
2.3.2 自定义装饰线样式
import android.text.style.LineBackgroundSpan
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF

// 自定义波浪下划线Span
class WavyUnderlineSpan(
    private val color: Int,
    private val strokeWidth: Float
) : LineBackgroundSpan {
    override fun drawBackground(
        canvas: Canvas,
        paint: Paint,
        left: Int,
        right: Int,
        top: Int,
        baseline: Int,
        bottom: Int,
        text: CharSequence,
        start: Int,
        end: Int,
        lineNumber: Int
    ) {
        val originalColor = paint.color
        val originalStyle = paint.style
        val originalStrokeWidth = paint.strokeWidth
        
        paint.color = color
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = strokeWidth
        
        // 绘制波浪线
        val path = android.graphics.Path()
        val waveWidth = 10f
        val waveHeight = 5f
        var x = left.toFloat()
        val y = baseline + 5f
        
        path.moveTo(x, y)
        while (x < right) {
            path.quadTo(
                x + waveWidth / 2, y - waveHeight,
                x + waveWidth, y
            )
            x += waveWidth
        }
        
        canvas.drawPath(path, paint)
        
        paint.color = originalColor
        paint.style = originalStyle
        paint.strokeWidth = originalStrokeWidth
    }
}

@Composable
fun CustomDecorationExample() {
    val customDecoratedText = buildSpannedString {
        append("普通文本 ")
        inSpans(WavyUnderlineSpan(Color.Red.toArgb(), 3f)) {
            append("波浪下划线")
        }
    }
    
    Text(text = customDecoratedText.toAnnotatedString())
}

2.4 字间距与行高设置

2.4.1 字间距(Letter Spacing)
import android.text.style.ScaleXSpan
import android.text.style.TrackingSpan

@Composable
fun LetterSpacingExample() {
    val spacedText = buildSpannedString {
        append("正常间距 ")
        
        // 使用ScaleXSpan实现字间距(拉伸字符)
        inSpans(ScaleXSpan(1.5f)) {
            append("宽间距")
        }
        
        append(" ")
        
        // 紧凑间距
        inSpans(ScaleXSpan(0.8f)) {
            append("紧凑间距")
        }
    }
    
    Text(text = spacedText.toAnnotatedString())
}
2.4.2 行高设置
import android.text.style.LineHeightSpan
import android.text.StaticLayout

// 自定义行高Span
class CustomLineHeightSpan(private val height: Int) : LineHeightSpan {
    override fun chooseHeight(
        text: CharSequence,
        start: Int,
        end: Int,
        spanstartv: Int,
        lineHeight: Int,
        fm: Paint.FontMetricsInt
    ) {
        val originalHeight = fm.descent - fm.ascent
        if (originalHeight < height) {
            val extra = height - originalHeight
            fm.descent += extra / 2
            fm.ascent -= extra / 2
        }
    }
}

@Composable
fun LineHeightExample() {
    val lineHeightText = buildSpannedString {
        inSpans(CustomLineHeightSpan(80)) {
            append("第一行文字\n")
            append("第二行文字\n")
            append("第三行文字")
        }
    }
    
    Text(
        text = lineHeightText.toAnnotatedString(),
        lineHeight = 2.em  // Compose层面的行高设置
    )
}

2.5 段落对齐方式

2.5.1 段落级Span
import android.text.style.AlignmentSpan
import android.text.Layout

@Composable
fun ParagraphAlignmentExample() {
    val alignedText = buildSpannedString {
        // 左对齐段落
        inSpans(AlignmentSpan.Standard(Layout.Alignment.ALIGN_NORMAL)) {
            append("这是左对齐的段落文本\n")
        }
        
        // 居中对齐段落
        inSpans(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER)) {
            append("这是居中对齐的段落文本\n")
        }
        
        // 右对齐段落
        inSpans(AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE)) {
            append("这是右对齐的段落文本")
        }
    }
    
    Text(
        text = alignedText.toAnnotatedString(),
        modifier = Modifier.fillMaxWidth()
    )
}
2.5.2 首行缩进与段落间距
import android.text.style.LeadingMarginSpan
import android.text.style.LineHeightSpan

@Composable
fun ParagraphIndentExample() {
    val indentedText = buildSpannedString {
        // 首行缩进
        inSpans(LeadingMarginSpan.Standard(60, 30)) {
            append("这是一个带有首行缩进的段落。第一行会有较大的缩进,后续行缩进较小。这种格式常用于文章正文的排版。\n\n")
        }
        
        // 项目符号样式
        inSpans(LeadingMarginSpan.Standard(40)) {
            append("• 项目符号第一项\n")
            append("• 项目符号第二项\n")
            append("• 项目符号第三项")
        }
    }
    
    Text(text = indentedText.toAnnotatedString())
}

2.6 上下标与URL链接

2.6.1 上标与下标
import androidx.core.text.superscript
import androidx.core.text.subscript
import android.text.style.SuperscriptSpan
import android.text.style.SubscriptSpan

@Composable
fun ScriptExample() {
    val scriptText = buildSpannedString {
        append("化学公式: H")
        subscript { append("2") }
        append("O\n")
        
        append("数学公式: x")
        superscript { append("2") }
        append(" + y")
        superscript { append("2") }
        append(" = z")
        superscript { append("2") }
    }
    
    Text(text = scriptText.toAnnotatedString())
}
2.6.2 可点击链接
import android.text.style.ClickableSpan
import android.text.style.URLSpan
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.foundation.text.ClickableText

@Composable
fun ClickableLinkExample() {
    val uriHandler = LocalUriHandler.current
    
    val linkText = buildSpannedString {
        append("访问我们的")
        
        // URL链接
        inSpans(URLSpan("https://www.example.com")) {
            append("官方网站")
        }
        
        append("或")
        
        // 自定义点击Span
        inSpans(object : ClickableSpan() {
            override fun onClick(widget: android.view.View) {
                // 处理点击事件
            }
            
            override fun updateDrawState(ds: TextPaint) {
                super.updateDrawState(ds)
                ds.isUnderlineText = true
                ds.color = Color.Blue.toArgb()
            }
        }) {
            append("联系我们")
        }
    }
    
    // 使用ClickableText显示可点击文本
    ClickableText(
        text = linkText.toAnnotatedString(),
        onClick = { offset ->
            linkText.getSpans(offset, offset, URLSpan::class.java)
                .firstOrNull()?.let { span ->
                    uriHandler.openUri(span.url)
                }
        }
    )
}

三、代码示例

示例1:富文本新闻标题(基础样式组合)

/**
 * 示例1:富文本新闻标题
 * 展示基础样式的组合使用:粗体、颜色、大小、下划线
 */
@Composable
fun RichNewsTitleExample() {
    // 构建富文本新闻标题
    val newsTitle = buildSpannedString {
        // 新闻分类标签 - 蓝色背景白色文字
        backgroundColor(Color(0xFF1976D2)) {
            color(Color.White) {
                scale(0.85f) {
                    append(" 热点 ")
                }
            }
        }
        
        append(" ")
        
        // 主标题 - 粗体大号
        bold {
            scale(1.3f) {
                append("Jetpack Compose")
            }
        }
        
        // 副标题 - 普通大小,不同颜色
        color(Color.Gray) {
            append(" 正式发布")
        }
        
        append(" ")
        
        // 重要标记 - 红色下划线
        underline {
            color(Color.Red) {
                scale(0.9f) {
                    append("重要")
                }
            }
        }
    }
    
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        // 标题显示
        Text(
            text = newsTitle.toAnnotatedString(),
            fontSize = 20.sp,
            lineHeight = 32.sp
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // 分隔线
        Divider()
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // 正文预览 - 使用删除线表示旧内容
        val previewText = buildSpannedString {
            append("Google宣布")
            strikeThrough {
                color(Color.Gray) {
                    append("Android View系统")
                }
            }
            append(" ")
            bold {
                color(Color(0xFF1976D2)) {
                    append("Jetpack Compose")
                }
            }
            append("成为现代Android UI开发的首选方案...")
        }
        
        Text(
            text = previewText.toAnnotatedString(),
            fontSize = 14.sp,
            color = Color.DarkGray,
            lineHeight = 22.sp
        )
    }
}

示例2:商品详情价格展示(复杂样式交互)

/**
 * 示例2:商品详情价格展示
 * 展示复杂样式:删除线原价、彩色折扣标签、多种字体大小组合
 */
@Composable
fun ProductPriceExample() {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        // 商品名称
        val productName = buildSpannedString {
            bold {
                scale(1.2f) {
                    append("无线蓝牙耳机 Pro")
                }
            }
            append(" ")
            scale(0.8f) {
                color(Color.Gray) {
                    append("旗舰版")
                }
            }
        }
        
        Text(
            text = productName.toAnnotatedString(),
            modifier = Modifier.padding(bottom = 8.dp)
        )
        
        // 价格区域
        Row(
            verticalAlignment = Alignment.Bottom,
            modifier = Modifier.padding(vertical = 8.dp)
        ) {
            // 折扣标签
            val discountTag = buildSpannedString {
                backgroundColor(Color(0xFFFF5722)) {
                    color(Color.White) {
                        bold {
                            scale(0.85f) {
                                append(" -30% ")
                            }
                        }
                    }
                }
            }
            
            Text(
                text = discountTag.toAnnotatedString(),
                modifier = Modifier.padding(end = 8.dp)
            )
            
            // 现价
            val currentPrice = buildSpannedString {
                color(Color(0xFFFF5722)) {
                    bold {
                        scale(1.5f) {
                            append("¥299")
                        }
                    }
                }
            }
            
            Text(text = currentPrice.toAnnotatedString())
            
            Spacer(modifier = Modifier.width(12.dp))
            
            // 原价(删除线)
            val originalPrice = buildSpannedString {
                strikeThrough {
                    color(Color.Gray) {
                        scale(0.9f) {
                            append("¥428")
                        }
                    }
                }
            }
            
            Text(text = originalPrice.toAnnotatedString())
        }
        
        // 促销信息
        val promotionInfo = buildSpannedString {
            append("限时优惠:")
            
            // 倒计时样式
            backgroundColor(Color(0xFFFFF3E0)) {
                color(Color(0xFFFF5722)) {
                    bold {
                        append("23:59:59")
                    }
                }
            }
            
            append(" 后恢复原价 | ")
            
            underline {
                color(Color(0xFF1976D2)) {
                    append("查看详情 >")
                }
            }
        }
        
        Text(
            text = promotionInfo.toAnnotatedString(),
            fontSize = 12.sp,
            modifier = Modifier.padding(top = 8.dp)
        )
        
        Divider(modifier = Modifier.padding(vertical = 16.dp))
        
        // 规格信息
        val specsText = buildSpannedString {
            // 使用项目符号样式
            append("• ")
            bold { append("颜色: ") }
            append("曜石黑 / 珍珠白 / 深海蓝\n")
            
            append("• ")
            bold { append("续航: ") }
            append("单次8小时,配合充电仓可达32小时\n")
            
            append("• ")
            bold { append("特色: ") }
            italic {
                color(Color(0xFF4CAF50)) {
                    append("主动降噪 · 空间音频 · 通透模式")
                }
            }
        }
        
        Text(
            text = specsText.toAnnotatedString(),
            fontSize = 13.sp,
            lineHeight = 24.sp,
            color = Color.DarkGray
        )
    }
}

示例3:学术论文摘要(段落级样式)

/**
 * 示例3:学术论文摘要展示
 * 展示段落级样式:首行缩进、行高、对齐方式、上下标
 */
@Composable
fun AcademicPaperExample() {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        // 论文标题
        val paperTitle = buildSpannedString {
            bold {
                scale(1.15f) {
                    append("基于深度学习的图像识别算法研究")
                }
            }
        }
        
        Text(
            text = paperTitle.toAnnotatedString(),
            fontSize = 18.sp,
            textAlign = TextAlign.Center,
            modifier = Modifier.fillMaxWidth()
        )
        
        Spacer(modifier = Modifier.height(8.dp))
        
        // 作者信息
        val authorInfo = buildSpannedString {
            color(Color.Gray) {
                scale(0.9f) {
                    append("张三")
                }
            }
            superscript { scale(0.7f) { append("1") } }
            
            append(",")
            
            color(Color.Gray) {
                scale(0.9f) {
                    append("李四")
                }
            }
            superscript { scale(0.7f) { append("2,*") } }
            
            append(",")
            
            color(Color.Gray) {
                scale(0.9f) {
                    append("王五")
                }
            }
            superscript { scale(0.7f) { append("1") } }
        }
        
        Text(
            text = authorInfo.toAnnotatedString(),
            textAlign = TextAlign.Center,
            modifier = Modifier.fillMaxWidth()
        )
        
        // 单位信息
        val affiliationInfo = buildSpannedString {
            scale(0.8f) {
                color(Color.Gray) {
                    superscript { append("1 ") }
                    append("计算机科学与技术学院,某某大学,北京 100000\n")
                    superscript { append("2 ") }
                    append("人工智能研究院,某某实验室,上海 200000\n")
                    superscript { append("* ") }
                    append("通讯作者: lisi@example.com")
                }
            }
        }
        
        Text(
            text = affiliationInfo.toAnnotatedString(),
            textAlign = TextAlign.Center,
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = 4.dp),
            lineHeight = 18.sp
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        Divider()
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // 摘要标题
        val abstractTitle = buildSpannedString {
            bold {
                backgroundColor(Color(0xFFE3F2FD)) {
                    append(" 摘 要 ")
                }
            }
        }
        
        Text(
            text = abstractTitle.toAnnotatedString(),
            fontSize = 14.sp,
            modifier = Modifier.padding(bottom = 8.dp)
        )
        
        // 摘要内容 - 首行缩进
        val abstractContent = buildSpannedString {
            inSpans(LeadingMarginSpan.Standard(60, 0)) {
                append("本文提出了一种基于")
                bold {
                    color(Color(0xFF1976D2)) {
                        append("卷积神经网络(CNN)")
                    }
                }
                append("和")
                bold {
                    color(Color(0xFF1976D2)) {
                        append("注意力机制")
                    }
                }
                append("相结合的图像识别算法。")
                
                // 公式引用
                superscript { append("[1]") }
                
                append("通过在标准数据集ImageNet上的实验验证,该算法的识别准确率达到了")
                
                // 突出显示数据
                backgroundColor(Color(0xFFFFF9C4)) {
                    bold {
                        append("94.2%")
                    }
                }
                
                append(",相比传统方法提升了")
                italic { append("3.5个百分点") }
                append("。实验结果表明,该方法在")
                underline { append("复杂背景") }
                append("和")
                underline { append("多目标场景") }
                append("下具有良好的鲁棒性和泛化能力。")
            }
        }
        
        Text(
            text = abstractContent.toAnnotatedString(),
            fontSize = 13.sp,
            lineHeight = 24.sp,
            textAlign = TextAlign.Justify
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // 关键词
        val keywords = buildSpannedString {
            bold { append("关键词:") }
            
            backgroundColor(Color(0xFFF5F5F5)) {
                scale(0.95f) {
                    append("深度学习")
                }
            }
            append(";")
            
            backgroundColor(Color(0xFFF5F5F5)) {
                scale(0.95f) {
                    append("图像识别")
                }
            }
            append(";")
            
            backgroundColor(Color(0xFFF5F5F5)) {
                scale(0.95f) {
                    append("卷积神经网络")
                }
            }
            append(";")
            
            backgroundColor(Color(0xFFF5F5F5)) {
                scale(0.95f) {
                    append("注意力机制")
                }
            }
        }
        
        Text(
            text = keywords.toAnnotatedString(),
            fontSize = 12.sp,
            modifier = Modifier.padding(top = 8.dp)
        )
    }
}

四、与AnnotatedString的对比

4.1 API风格对比

特性buildSpannedStringAnnotatedString
平台Android原生Jetpack Compose跨平台
返回类型SpannableStringAnnotatedString
DSL风格函数式嵌套Builder模式
Compose集成需转换原生支持
Span类型Android原生SpanCompose SpanStyle
性能依赖View系统Compose优化
可点击ClickableSpanLinkAnnotation

4.2 代码风格对比

// ==================== buildSpannedString ====================
val spannedText = buildSpannedString {
    append("Hello ")
    bold { 
        color(Color.Red) {
            append("World")
        }
    }
}

Text(text = spannedText.toAnnotatedString())

// ==================== AnnotatedString ====================
val annotatedText = buildAnnotatedString {
    append("Hello ")
    withStyle(
        style = SpanStyle(
            color = Color.Red,
            fontWeight = FontWeight.Bold
        )
    ) {
        append("World")
    }
}

Text(text = annotatedText)

4.3 适用场景对比

/**
 * 选择建议:
 * 
 * 使用 buildSpannedString 的场景:
 * 1. 需要与Android原生View系统交互
 * 2. 需要使用特定的Android Span类型
 * 3. 迁移旧项目代码
 * 4. 需要ClickableSpan的特定行为
 */

// 场景1:与TextView原生组件交互
fun createNativeText(): SpannableString {
    return buildSpannedString {
        bold { append("原生TextView使用") }
    }
}

/**
 * 使用 AnnotatedString 的场景:
 * 1. 纯Compose项目
 * 2. 跨平台需求(Compose Multiplatform)
 * 3. 需要Compose特有的样式属性
 * 4. 更好的性能优化
 */

// 场景2:纯Compose项目
@Composable
fun ComposeOnlyText() {
    val text = buildAnnotatedString {
        withStyle(SpanStyle(brush = Brush.linearGradient(...))) {
            append("渐变文字")
        }
    }
    Text(text = text)
}

4.4 转换方法

/**
 * SpannedString 转 AnnotatedString
 */
fun Spanned.toAnnotatedString(): AnnotatedString {
    return buildAnnotatedString {
        append(this@toAnnotatedString.toString())
        
        // 转换所有Span
        getSpans(0, length, Any::class.java).forEach { span ->
            val start = getSpanStart(span)
            val end = getSpanEnd(span)
            
            when (span) {
                is StyleSpan -> {
                    val style = when (span.style) {
                        Typeface.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
                        Typeface.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
                        Typeface.BOLD_ITALIC -> SpanStyle(
                            fontWeight = FontWeight.Bold,
                            fontStyle = FontStyle.Italic
                        )
                        else -> null
                    }
                    style?.let { addStyle(it, start, end) }
                }
                
                is ForegroundColorSpan -> {
                    addStyle(
                        SpanStyle(color = Color(span.foregroundColor)),
                        start, end
                    )
                }
                
                is BackgroundColorSpan -> {
                    addStyle(
                        SpanStyle(background = Color(span.backgroundColor)),
                        start, end
                    )
                }
                
                is UnderlineSpan -> {
                    addStyle(
                        SpanStyle(textDecoration = TextDecoration.Underline),
                        start, end
                    )
                }
                
                is StrikethroughSpan -> {
                    addStyle(
                        SpanStyle(textDecoration = TextDecoration.LineThrough),
                        start, end
                    )
                }
                
                is RelativeSizeSpan -> {
                    addStyle(
                        SpanStyle(fontSize = (span.sizeChange * 16).sp),
                        start, end
                    )
                }
                
                // 其他Span类型...
            }
        }
    }
}

/**
 * AnnotatedString 转 SpannedString(反向转换)
 */
fun AnnotatedString.toSpannedString(): SpannableString {
    val spannable = SpannableString(this.text)
    
    spanStyles.forEach { (style, start, end) ->
        // 转换SpanStyle到Android Span
        style.fontWeight?.let {
            if (it == FontWeight.Bold) {
                spannable.setSpan(
                    StyleSpan(Typeface.BOLD),
                    start, end,
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                )
            }
        }
        
        style.color?.let {
            spannable.setSpan(
                ForegroundColorSpan(it.toArgb()),
                start, end,
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
            )
        }
        
        // 其他属性转换...
    }
    
    return spannable
}

五、性能优化与最佳实践

5.1 性能注意事项

5.1.1 避免频繁重建
// ❌ 错误:每次重组都重新构建SpannedString
@Composable
fun BadExample(dynamicText: String) {
    Text(
        text = buildSpannedString {
            bold { append(dynamicText) }  // 每次重组都重建
        }.toAnnotatedString()
    )
}

// ✅ 正确:使用remember缓存
@Composable
fun GoodExample(dynamicText: String) {
    val spannedText = remember(dynamicText) {
        buildSpannedString {
            bold { append(dynamicText) }
        }.toAnnotatedString()
    }
    
    Text(text = spannedText)
}

// ✅ 更好:静态文本提取为常量
object StaticTexts {
    val HEADER = buildSpannedString {
        bold { append("固定标题") }
    }.toAnnotatedString()
}
5.1.2 转换性能优化
// ❌ 避免多次转换
@Composable
fun InefficientExample() {
    val spanned = buildSpannedString { /* ... */ }
    
    // 多次调用toAnnotatedString()
    Text(text = spanned.toAnnotatedString())
    Text(text = spanned.toAnnotatedString())
    Text(text = spanned.toAnnotatedString())
}

// ✅ 只转换一次
@Composable
fun EfficientExample() {
    val annotated = remember {
        buildSpannedString { /* ... */ }.toAnnotatedString()
    }
    
    Text(text = annotated)
    Text(text = annotated)
    Text(text = annotated)
}

5.2 内存优化

/**
 * 大型文本分段处理
 */
@Composable
fun LargeTextOptimization() {
    // 对于超长文本,分段构建避免内存峰值
    val chunks = remember {
        listOf(
            buildSpannedString { /* 段落1 */ },
            buildSpannedString { /* 段落2 */ },
            buildSpannedString { /* 段落3 */ }
        )
    }
    
    LazyColumn {
        items(chunks) { chunk ->
            Text(text = chunk.toAnnotatedString())
        }
    }
}

/**
 * 使用StringBuilder复用
 */
fun optimizedStringBuilding(): SpannableString {
    val stringBuilder = StringBuilder()
    
    return buildSpannedString {
        // 先构建基础文本
        stringBuilder.apply {
            append("段落1")
            append("段落2")
            append("段落3")
        }
        
        append(stringBuilder.toString())
        
        // 再应用样式
        setSpan(
            StyleSpan(Typeface.BOLD),
            0, 3,
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
        )
    }
}

5.3 最佳实践清单

/**
 * buildSpannedString最佳实践
 */
object SpannedStringBestPractices {
    
    /**
     * 1. 静态文本常量化
     */
    val STATIC_HEADER = buildSpannedString {
        bold { append("固定头部") }
    }.toAnnotatedString()
    
    /**
     * 2. 复杂样式封装为函数
     */
    fun SpannableStringBuilder.highlightedText(
        text: String,
        color: Color = Color.Red
    ) {
        backgroundColor(color.copy(alpha = 0.3f)) {
            bold { append(text) }
        }
    }
    
    /**
     * 3. 延迟初始化大型文本
     */
    class LazySpannedText {
        val content by lazy {
            buildSpannedString {
                // 复杂构建逻辑
            }.toAnnotatedString()
        }
    }
    
    /**
     * 4. 使用自定义Span减少嵌套
     */
    class CombinedStyleSpan(
        private val isBold: Boolean,
        private val textColor: Int,
        private val bgColor: Int
    ) : CharacterStyle() {
        override fun updateDrawState(tp: TextPaint?) {
            tp?.apply {
                if (isBold) typeface = Typeface.DEFAULT_BOLD
                color = textColor
                bgColor.let { bg ->
                    // 设置背景色
                }
            }
        }
    }
}

5.4 调试技巧

/**
 * SpannedString调试工具
 */
object SpannedStringDebugger {
    
    /**
     * 打印所有Span信息
     */
    fun Spanned.dumpSpans(): String {
        val sb = StringBuilder()
        sb.appendLine("Spanned内容: $this")
        sb.appendLine("Span数量: ${getSpans(0, length, Any::class.java).size}")
        sb.appendLine("---")
        
        getSpans(0, length, Any::class.java).forEachIndexed { index, span ->
            val start = getSpanStart(span)
            val end = getSpanEnd(span)
            val flags = getSpanFlags(span)
            
            sb.appendLine("Span #$index:")
            sb.appendLine("  类型: ${span::class.simpleName}")
            sb.appendLine("  范围: [$start, $end)")
            sb.appendLine("  内容: '${substring(start, end)}'")
            sb.appendLine("  标志: $flags")
            sb.appendLine()
        }
        
        return sb.toString()
    }
    
    /**
     * 验证Span覆盖范围
     */
    fun Spanned.validateSpans(): Boolean {
        val spans = getSpans(0, length, Any::class.java)
        
        // 检查是否有越界Span
        spans.forEach { span ->
            val start = getSpanStart(span)
            val end = getSpanEnd(span)
            
            if (start < 0 || end > length || start > end) {
                return false
            }
        }
        
        return true
    }
}

// 使用示例
@Composable
fun DebugExample() {
    val spanned = remember {
        buildSpannedString {
            bold { append("调试") }
            append("文本")
        }
    }
    
    // 打印调试信息
    LaunchedEffect(Unit) {
        Log.d("SpannedDebug", spanned.dumpSpans())
    }
    
    Text(text = spanned.toAnnotatedString())
}

总结

buildSpannedString是Android平台上强大的富文本构建工具,在Jetpack Compose中通过与AnnotatedString的转换,可以实现丰富的文本样式效果。

核心要点回顾:

  1. 基础使用:掌握bolditaliccolor等便捷扩展函数
  2. 高级样式:了解自定义Span的实现方法
  3. 段落控制:使用LeadingMarginSpanAlignmentSpan等段落级Span
  4. 平台选择:根据项目需求选择buildSpannedStringAnnotatedString
  5. 性能优化:避免频繁重建,合理使用rememberlazy

选择建议:

  • 新项目:优先使用AnnotatedString,更好的Compose集成
  • 混合项目:使用buildSpannedString + 转换函数
  • 迁移项目:逐步替换为AnnotatedString

通过本文的指南和示例,您应该能够在Jetpack Compose项目中灵活运用buildSpannedString创建丰富的文本效果。


参考资源