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

在 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、Canvas 和 Paint 有更深入的了解。从 Compose 1.2.0 开始,我们有了一个更好的解决方案!
Brush

Compose 1.2.0 向 TextStyle 和 SpanStyle 添加了 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围绕一个中心点从0到360度的渐变,就像一个雷达或钟表指针扫过一圈。
此外,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 可与所有接受 TextStyle 和 AnnotatedString 的元素一起使用。
例如,可以将笔刷配置为 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 习惯用法的新方法,在 TextStyle 和 SpanStyle 中使用 Brush等,这使得为文本添加复杂的颜色变化变得前所未有的简单。
Compose 在不断的推出令人兴奋的、符合 Compose 习惯用法的新 API,这些 API 能与我们已经熟悉的 API 无缝集成,帮助我们创建精美的视觉效果。