酷炫的文字效果 — 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 无缝集成,帮助我们创建精美的视觉效果。

相关推荐
菠菠萝宝4 小时前
【AI应用探索】-10- Cursor实战:小程序&APP - 下
人工智能·小程序·kotlin·notepad++·ai编程·cursor
RainbowC05 小时前
从Dalvik字节码角度优化安卓编码
android·java/jvm
河铃旅鹿5 小时前
Android开发-java版:布局
android·笔记·学习
Meteors.6 小时前
安卓进阶——RxJava
android·rxjava
drsonxu8 小时前
Android开发自学笔记 --- 构建简单的UI视图
android·compose
onthewaying9 小时前
在Android平台上使用Three.js优雅的加载3D模型
android·前端·three.js
带电的小王10 小时前
Android设备:无busybox工具解决
android·busybox
默契之行10 小时前
为什么要使用 .asStateFlow() 而不是直接赋值?
kotlin
一 乐10 小时前
个人健康系统|健康管理|基于java+Android+微信小程序的个人健康系统设计与实现(源码+数据库+文档)
android·java·数据库·vue.js·spring boot·生活
百锦再11 小时前
第14章 智能指针
android·java·开发语言·git·rust·go·错误