视觉篇:Canvas 自定义绘图与高级动画的华丽圆舞曲

在前面的篇幅里,我们用 Compose 搭建起了 App 的骨架、神经和肌肉。现在,我们要给它穿上最华丽的"定制礼服"。

在 CSDN 的技术分享中,能写出复杂业务逻辑的人很多,但能随心所欲绘制出令人惊艳的自定义 UI 的人却凤毛麟角。标准的 RowColumn 只能满足 80% 的需求,剩下的那 20%------那些让用户发出"Wow"赞叹的时刻,往往需要你跳出框架,在一张白纸上重新创作。

今天这一篇,我们就来探索 Compose 最具艺术感的领域:Canvas 自定义绘图与高级动画的结合。


视觉篇:Canvas 自定义绘图与高级动画的华丽圆舞曲

导语:抛弃 onDraw 的历史包袱

老 Android 开发提到自定义 View,脑子里立刻浮现出的是复杂的 View.onDraw(Canvas) 重写、难懂的 Paint 标志位,还有那一堆需要自己计算的坐标系。

Compose 的 Canvas 是对这一切的彻底革命。 它不是一个 View,而是一个 Composable 函数。它提供了一个名为 DrawScope 的声明式绘图环境,你只需要描述"现在该画什么",剩下的交给状态驱动机制。这就像从手动操作机床升级到了数控编程。


一、 初舞台:DrawScope,你的新画室

在 Compose 里使用 Canvas 非常简单。Canvas 组件提供了一个 lambda 块,这个块的接收者就是 DrawScope

核心优势

  1. 声明式: 你不需要手动调用 invalidate()。只要 Canvas 依赖的状态变了,它就会自动重绘。
  2. 极简 API: drawCircle, drawRect, drawLine... API 设计直觉且现代,无需繁琐地配置 Paint 对象。
  3. 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 块里进行复杂的数学计算、对象创建(如 PathPaint)或资源加载。
  • ✅ 推荐: 所有能预先计算好的数据,都在 Composable 函数体里算好,用 remember 存起来,只把最终结果交给 Canvas。

结语:不仅要好看,更要"耐打"

掌握了 Canvas 和高级动画,你就可以在 Android 的屏幕上为所欲为了。你可以复刻任何复杂的金融图表,实现任何设计师天马行空的动效。

但是,画得再好看,如果一上线就崩溃,那也是白搭。当我们的 UI 逻辑变得如此复杂,如何确保它的稳定性?

互动时间:
你曾经在旧 View 体系中实现过什么让你特别痛苦的自定义动画?如果现在用 Compose 的 Canvas 来重写,你觉得会轻松多少?欢迎在评论区晒出你的创意!


下一篇预告: 《质量篇:防御式编程,编写"牢不可破"的 Compose 单元测试》


这是第八篇的内容。

这一篇视觉冲击力很强,从基础绘图讲到了复杂的组合动画,是提升专栏"逼格"的关键章节。

相关推荐
Fushize8 小时前
多模块架构下的依赖治理:如何避免 Gradle 依赖地狱
android·架构·kotlin
Jomurphys8 小时前
Kotlin - 类型别名 typealias
android·kotlin
方见华Richard8 小时前
自指-认知几何架构 可行性边界白皮书(务实版)
人工智能·经验分享·交互·原型模式·空间计算
Haha_bj8 小时前
Flutter ——flutter_screenutil 屏幕适配
android·ios
Haha_bj9 小时前
Flutter ——device_info_plus详解
android·flutter·ios
前端小伙计9 小时前
Android/Flutter 项目统一构建配置最佳实践
android·flutter
LaughingZhu9 小时前
Product Hunt 每日热榜 | 2026-02-08
大数据·人工智能·经验分享·搜索引擎·产品运营
Mr_sun.10 小时前
Day09——入退管理-入住-2
android·java·开发语言
ujainu11 小时前
告别杂乱!Flutter + OpenHarmony 鸿蒙记事本的标签与分类管理(三)
android·flutter·openharmony