酷炫的文字效果 — Compose 文本着色

想象一下,设计师要求你实现下面的草图:

在 Jetpack Compose 中构建这个屏幕布局应该很简单,不过处理文本的渐变颜色,就没那么容易了。

第一种方法可以是使用 Compose 的 Canvas,直接绘制到原生的 android.graphics.Canvas 上:

Kotlin 复制代码
Canvas(...) {
   paint.apply {
       // 配置 paint
   }
   drawIntoCanvas { canvas ->
       // 配置 canvas
       // 绘制文字
       canvas.nativeCanvas.drawText(text, x, y, paint)
       //...
   }
   paint.reset()
}

一种更符合 Compose 习惯用法的方法是在 Text 上结合 Brush使用 drawWithCache 修饰符:

Kotlin 复制代码
val rainbowColors = listOf(
    Color(0xFFFF0000), // Red
    Color(0xFFFF7F00), // Orange
    Color(0xFFFFFF00), // Yellow
    Color(0xFF00FF00), // Green
    Color(0xFF00FFFF), // Cyan
    Color(0xFF0000FF), // Blue
    Color(0xFF8B00FF)  // Purple (or Violet)
)

//....

Text(
    text = "Text in Compose ❤️",
    fontSize = 36.sp,
    modifier = Modifier
        .graphicsLayer(alpha = 0.99f)
        .drawWithCache {
            val brush = Brush.horizontalGradient(rainbowColors)
            onDrawWithContent {
                drawContent()
                drawRect(brush, blendMode = BlendMode.SrcAtop)
            }
        }
)

效果如下:

这里的策略是在文本上方绘制一个带有渐变颜色的矩形,然后使用 SrcAtop 进行混合,以确保只显示文本,而矩形的其余部分被裁剪掉。然而,这种方法会绘制在表情符号(如你在上面看到的)和内联内容上。

这两种解决方案都需要对绘图 API、CanvasPaint 有更深入的了解。从 Compose 1.2.0 开始,我们有了一个更好的解决方案!

Brush

Compose 1.2.0 向 TextStyleSpanStyle 添加了 Brush API,提供了一种绘制具有复杂颜色的文本的方法,而渐变仅仅只是个开始。

你将使用两个主要组件:

  • Brush(笔刷):它提供了对默认笔刷的访问,其中大多数是 ShaderBrush 的实现(如 LinearGradient(线性渐变)、RadialGradient(径向渐变)等)。
  • ShaderBrush(着色器笔刷):当默认笔刷不够用时,你可以扩展这个类来实现自己的自定义笔刷。

为了实现上述设计,我们定义渐变颜色列表,并在 TextStyle 上使用 linearGradient 笔刷。

我们对上述代码进行简单的修改,使用 TextStyle 去实现:

Kotlin 复制代码
Text(
    text = "Text in Compose ❤️",
    fontSize = 36.sp,
    style = TextStyle(
        brush = Brush.linearGradient(
            colors = rainbowColors
        )
    )
)

就是这样!真的就这么简单。

注意看 emoji 表情,使用这个解决方案,笔刷不会在表情符号上绘制,因为底层着色器会跳过它们。

默认 Brush

Brush 在其 API 中提供了各种预定义的笔刷样式。我们已经使用了 linearGradient,此外你还可以使用以下几种:

横向笔刷,竖向笔刷。

放射笔刷,扫掠笔刷。

sweepGradient 围绕一个中心点从 0360 度的渐变,就像一个雷达或钟表指针扫过一圈。

此外,SolidColor 笔刷使用单一指定颜色进行绘制:

Kotlin 复制代码
Text(
    text = text,
    fontSize = 36.sp,
    style = TextStyle(
        brush = SolidColor(Color.Cyan)
    )
)

自定义 Brush

在某些情况下,你可能需要确切知道笔刷的大小或绘图区域,并据此进行一些计算,比如缩小笔刷尺寸以实现特定的平铺效果。

下面让我们看看如何使用自定义笔刷来实现这一点。

重复颜色

假设我们想要实现某种颜色图案重复三次。一个简单的方法是将笔刷大小缩小到绘图区域的三分之一,然后重复该序列。

要获取笔刷大小,你可以通过扩展抽象类 ShaderBrush 并重写 createShader() 方法来创建自己的笔刷:

Kotlin 复制代码
class ScaledThirdBrush(val shaderBrush: ShaderBrush): ShaderBrush() {
    override fun createShader(size: Size): Shader {
        return shaderBrush.createShader(size / 3f)
    }
}

//...

Text(
    text = text,
    fontSize = 36.sp,
    style = TextStyle(
        brush = ScaledThirdBrush(Brush.linearGradient(
            colors = rainbowColors,
            tileMode = TileMode.Repeated
        ) as ShaderBrush)
    )
)

渐变图案会按照 tileMode 参数给定的策略重复。

tileMode 参数决定了着色器填充超出其边界区域的行为方式。由于重复方式是经过计算的,因此在以下情况下更容易注意到这种效果:

  • 强制使笔刷小于文本布局(就像在这种情况下)。
  • 绘图坐标小于可用的绘图区域。
  • 使用径向渐变笔刷。

你可以使用以下平铺模式(tileMode):

  • repeated(上面刚用过):在边缘重新开始颜色序列,重复该序列。
  • mirror:在图案边缘将颜色从最后一个镜像到第一个。
  • clamp:通过用渐变边缘的颜色进行绘制来填充绘图区域:
  • decal 从 Android S(API 31)及更高版本开始受支持,它根据笔刷大小绘制图案,并用黑色填充绘图区域的其余部分。你可以使用 isSupported 方法检查此 tileMode 是否受支持。如果不支持,它将回退到 tileMode clamp 模式。

图案文本着色

假设我们需要将图像的颜色用作文本颜色。例如,我们期望得到这样的结果:

要实现这种效果,我们使用一种方法来创建一个 ShaderBrush,传入一个原生的 BitmapShader 并设置我们想要使用的 Bitmap

Kotlin 复制代码
val res = LocalResources.current

val brush = remember {
    ShaderBrush(
        BitmapShader(
            ImageBitmap.imageResource(res = res, id = R.drawable.bat).asAndroidBitmap(),
            android.graphics.Shader.TileMode.REPEAT,
            android.graphics.Shader.TileMode.REPEAT,
        ).apply {
            transform { postScale(1.06f,0.8f) } // 这里是为了缩放图片大小,最好是通过实际情况进行调整
        }
    )

}

Text(
    text = longText,
    fontSize = 14.sp,
    style = TextStyle(brush = brush)
)

我们使用 remember 保存 ShaderBrush,因为创建着色器可能开销很大,而且每次调用 ShaderBrush 也会为 BitmapShader 进行新的内存分配。

Brush 集成到文本

Brush 可与所有接受 TextStyleAnnotatedString 的元素一起使用。

例如,可以将笔刷配置为 TextField 的样式:

Kotlin 复制代码
var input by remember { mutableStateOf("") }
val inputBrush = remember {
   Brush.linearGradient(
      colors = rainbowColors
      )
   }

TextField(
   value = input,
   onValueChange = { input = it },
   textStyle = TextStyle(brush = inputBrush, fontSize = 24.sp)
)

TextField 的状态随着每次输入新字符而改变时,确保使用 remember 函数在重组过程中保留笔刷。

此外,系统会在底层进行一些性能优化。例如,将笔刷转换为着色器可能是一项开销很大的操作,但 AndroidTextPaint 会针对在重组之间不会改变的笔刷(就像在这种情况下)优化此过程。

如果仅为文本或段落的选定部分添加渐变效果,可以构建一个 AnnotatedString 并仅为文本的特定跨度设置笔刷样式,如下所示:

Kotlin 复制代码
Text(
    buildAnnotatedString {
        append("Do not allow people to dim your shine\n")
        withStyle(
            SpanStyle(
                brush = Brush.linearGradient(
                    colors = rainbowColors
                )
            )
        ) {
            append("because they are blinded.")
        }
        append("\nTell them to put some sunglasses on.")
    } ,
    fontSize = 36.sp
)

透明度

TextStyle/SpanStyle 可以设置透明度,当使用颜色渐变实现类似以下效果时,该参数允许你修改整个 Text 的不透明度:

Kotlin 复制代码
Text(
    buildAnnotatedString {
        withStyle(
            SpanStyle(
                brush = Brush.linearGradient(
                    colors = rainbowColors
                ),
                alpha = 0.2f // 设置透明度
            )
        ) {
            append("because they are blinded.")
        }

        withStyle(
            SpanStyle(
                brush = Brush.linearGradient(
                    colors = rainbowColors
                ),
                alpha = 0.8f // 设置透明度
            )
        ) {
            append("\nTell them to put some sunglasses on.")
        }

    } ,
    fontSize = 36.sp
)

对比很明显,不是吗?

总结

本文详细介绍了如何在 Jetpack Compose 中实现文本渐变效果的方法,包括直接使用 Canvas 进行绘制的传统方式,以及利用 Brush API提供的更符合 Compose 习惯用法的新方法,在 TextStyleSpanStyle 中使用 Brush等,这使得为文本添加复杂的颜色变化变得前所未有的简单。

Compose 在不断的推出令人兴奋的、符合 Compose 习惯用法的新 API,这些 API 能与我们已经熟悉的 API 无缝集成,帮助我们创建精美的视觉效果。

相关推荐
张小潇27 分钟前
AOSP15 Input专题InputDispatcher源码分析
android
TT_Close29 分钟前
【Flutter×鸿蒙】debug 包也要签名,这点和 Android 差远了
android·flutter·harmonyos
Kapaseker2 小时前
2026年,我们还该不该学编程?
android·kotlin
雨白18 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk18 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING19 小时前
RN容器启动优化实践
android·react native
恋猫de小郭21 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker1 天前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴1 天前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭2 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter