在前面的篇幅里,我们用 Compose 搭建起了 App 的骨架、神经和肌肉。现在,我们要给它穿上最华丽的"定制礼服"。
在 CSDN 的技术分享中,能写出复杂业务逻辑的人很多,但能随心所欲绘制出令人惊艳的自定义 UI 的人却凤毛麟角。标准的 Row 和 Column 只能满足 80% 的需求,剩下的那 20%------那些让用户发出"Wow"赞叹的时刻,往往需要你跳出框架,在一张白纸上重新创作。
今天这一篇,我们就来探索 Compose 最具艺术感的领域:Canvas 自定义绘图与高级动画的结合。
视觉篇:Canvas 自定义绘图与高级动画的华丽圆舞曲
导语:抛弃 onDraw 的历史包袱
老 Android 开发提到自定义 View,脑子里立刻浮现出的是复杂的 View.onDraw(Canvas) 重写、难懂的 Paint 标志位,还有那一堆需要自己计算的坐标系。
Compose 的 Canvas 是对这一切的彻底革命。 它不是一个 View,而是一个 Composable 函数。它提供了一个名为 DrawScope 的声明式绘图环境,你只需要描述"现在该画什么",剩下的交给状态驱动机制。这就像从手动操作机床升级到了数控编程。
一、 初舞台:DrawScope,你的新画室
在 Compose 里使用 Canvas 非常简单。Canvas 组件提供了一个 lambda 块,这个块的接收者就是 DrawScope。
核心优势
- 声明式: 你不需要手动调用
invalidate()。只要 Canvas 依赖的状态变了,它就会自动重绘。 - 极简 API:
drawCircle,drawRect,drawLine... API 设计直觉且现代,无需繁琐地配置 Paint 对象。 - DP 单位自动适配: 在
DrawScope里,你可以直接用.dp.toPx(),不用再自己折腾像素换算了。
实战:画一个带缺口的进度环
我们不画无聊的圆,来画一个更有设计感的"进度仪表盘"。
kotlin
@Composable
fun DashboardRing(progress: Float /* 0f 到 1f */) {
val primaryColor = MaterialTheme.colorScheme.primary
Canvas(modifier = Modifier.size(200.dp)) {
// DrawScope 内部
val strokeWidth = 20.dp.toPx()
val radius = size.minDimension / 2 - strokeWidth / 2
val center = Offset(size.width / 2, size.height / 2)
// 1. 画底色灰环(270度缺口环)
drawArc(
color = Color.LightGray.copy(alpha = 0.3f),
startAngle = 135f,
sweepAngle = 270f,
useCenter = false,
topLeft = Offset(center.x - radius, center.y - radius),
size = Size(radius * 2, radius * 2),
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
// 2. 画进度条(根据 progress 动态计算角度)
drawArc(
color = primaryColor,
startAngle = 135f,
sweepAngle = 270f * progress, // 核心:状态驱动绘图
useCenter = false,
topLeft = Offset(center.x - radius, center.y - radius),
size = Size(radius * 2, radius * 2),
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
}
}
看! 我们没有创建任何 Paint 对象,代码清晰得就像在读设计稿说明书。
二、 圆舞曲起步:让画面动起来
静态的画只是基础,动画才是灵魂。Compose 的动画哲学是:万物皆可动画,因为万物皆是状态。
1. 简单的过渡:animate*AsState
如果你只是想让上面的进度条从 0.5 平滑过渡到 0.8,用这个就够了。它就像一个"傻瓜相机",给你最平稳的效果。
kotlin
val animatedProgress by animateFloatAsState(
targetValue = currentProgress, // 你的业务状态
animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing)
)
DashboardRing(progress = animatedProgress)
2. 高级的编排:Animatable
如果你需要更复杂的控制,比如让一个光点沿着特定轨迹无限循环,或者多个动画按顺序播放,你就需要 Animatable 这位"编舞大师"。
三、 华丽终章:实战"雷达扫描"特效
让我们结合 Canvas 和高级动画,实现一个酷炫的雷达扫描效果:一根扫描线在旋转,同时有波纹在向外扩散。
这个效果需要两个独立的动画:旋转角度动画 + 波纹扩散动画。
kotlin
@Composable
fun RadarView() {
// 1. 定义动画状态(编舞者)
val rotationAnim = remember { Animatable(0f) }
val pulseAnim = remember { Animatable(0f) }
// 2. 启动动画循环(副作用)
LaunchedEffect(Unit) {
// 并行执行两个动画循环
launch {
rotationAnim.animateTo(
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(3000, easing = LinearEasing)
)
)
}
launch {
pulseAnim.animateTo(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
}
}
// 3. 在 Canvas 里挥洒创意
val radarColor = Color.Green
Canvas(modifier = Modifier.size(300.dp).background(Color.Black)) {
val center = Offset(size.width / 2, size.height / 2)
val maxRadius = size.minDimension / 2
// 画扩散波纹 (根据 pulseAnim 的值改变半径和透明度)
drawCircle(
color = radarColor.copy(alpha = 1f - pulseAnim.value),
radius = maxRadius * pulseAnim.value,
center = center,
style = Stroke(width = 2.dp.toPx())
)
// 画几个静态的参考圈
drawCircle(color = radarColor.copy(alpha = 0.5f), radius = maxRadius, style = Stroke(1.dp.toPx()))
drawCircle(color = radarColor.copy(alpha = 0.3f), radius = maxRadius * 0.6f, style = Stroke(1.dp.toPx()))
// 画旋转扫描线 (利用 rotate 变换)
rotate(degrees = rotationAnim.value, pivot = center) {
// 这里画一条带渐变的扫描扇形会更酷,为了简单先画线
drawLine(
color = radarColor,
start = center,
end = Offset(center.x, center.y - maxRadius),
strokeWidth = 3.dp.toPx(),
cap = StrokeCap.Round
)
}
}
}
效果: 你的屏幕上出现了一个充满科技感的、不断扫描的雷达。这一切的核心,只是两个不断变化的 float 状态值,驱动着 DrawScope 不断重绘出新的画面。这就是我所说的"圆舞曲"。
四、 性能画家的自我修养
在享受创作快感的同时,请记住一条铁律:DrawScope 里的代码执行频率极高(每秒可能 60-120 次)。
- ❌ 禁止: 在
Canvas块里进行复杂的数学计算、对象创建(如Path、Paint)或资源加载。 - ✅ 推荐: 所有能预先计算好的数据,都在 Composable 函数体里算好,用
remember存起来,只把最终结果交给 Canvas。
结语:不仅要好看,更要"耐打"
掌握了 Canvas 和高级动画,你就可以在 Android 的屏幕上为所欲为了。你可以复刻任何复杂的金融图表,实现任何设计师天马行空的动效。
但是,画得再好看,如果一上线就崩溃,那也是白搭。当我们的 UI 逻辑变得如此复杂,如何确保它的稳定性?
互动时间:
你曾经在旧 View 体系中实现过什么让你特别痛苦的自定义动画?如果现在用 Compose 的 Canvas 来重写,你觉得会轻松多少?欢迎在评论区晒出你的创意!
下一篇预告: 《质量篇:防御式编程,编写"牢不可破"的 Compose 单元测试》
这是第八篇的内容。
这一篇视觉冲击力很强,从基础绘图讲到了复杂的组合动画,是提升专栏"逼格"的关键章节。