用 KuiklyUI Canvas 打造天气预测图表

项目地址:GitHub

从最早的 Android 入门教程,到 iOS SwiftUI 官方示例,再到 React、Flutter、鸿蒙... 天气应用几乎是所有 UI 框架的"Hello World Plus"------它足够简单,能让你快速上手;又足够丰富,涵盖了数据展示、图表绑定等核心技能。

今天,我们用 Kuikly 来完成这个demo:用一套 Kotlin 代码,画一个能在 Android、iOS、鸿蒙三端运行的雨量预测图表

别担心,这篇文章专门为新手准备,我们会从最基础的概念讲起。准备好了吗?Let's go!

我们要做什么?

先看看最终效果:一张带毛玻璃效果的卡片,上面有一条蓝色的曲线,展示未来 90 分钟的雨量变化。左边是雨量等级(暴雨、大雨、中雨、小雨),底部是时间轴(现在、30分钟、60分钟、90分钟)。

是不是很眼熟?没错,这就是各大天气 App 里那个"分钟级降水预报"的同款!

第一步:搭建页面骨架

在 Kuikly 中,一个页面就是一个继承自 BasePager 的类:

kotlin 复制代码
@Page("WeatherCanvasPage")  // 给页面取个名字,用于路由跳转
internal class WeatherCanvasPage : BasePager() {
    
    override fun body(): ViewBuilder {
        return {
            // 页面内容写在这里
        }
    }
}

tips

  • @Page("xxx") 就像给页面起了个门牌号

  • body() 方法返回页面的 UI 结构,所有组件都写在大括号里

第二步:铺一张好看的背景图

天气应用嘛,氛围感很重要!

kotlin 复制代码
override fun body(): ViewBuilder {
    return {
        attr {
            flex(1f)  // flex 弹性布局
            backgroundColor(Color(0x436082, 1f))  // 蓝灰色底色
        }

        // 背景图
        Image {
            attr {
                src("https://qq-weather.cdn-go.cn/weather/latest/rain-bg/rain-comming.png")
                positionAbsolute()  // 绝对定位
                left(0f); right(0f); top(0f); bottom(0f)  // 铺满全屏
            }
        }
    }
}

tips

  • attr { } 用来设置组件样式,类似 CSS

  • positionAbsolute() + 四边为 0 = 铺满父容器

第三步:自定义组件

接下来我们要添加图表卡片。但 RainFallNodeCanvas 不是内置组件,而是我们自己造的!

这就是 Kuikly 的魅力:把复杂 UI 封装成可复用的积木块

一个自定义组件需要三样东西:

组成部分 作用
属性类 (Attr) 定义组件接收什么数据
组件类 (ComposeView) 定义组件长什么样
扩展函数 让组件用起来更顺手

3.1 定义属性:这个组件需要什么数据?

kotlin 复制代码
class RainFallNodeAttr : ComposeAttr() {
    // 雨量数据:24个点,代表未来90分钟的降水量
    var nodes: List<Float> by observable(
        listOf(1.3f, 2.6f, 4.2f, 5.1f, 3.5f, ...)
    )
    
    // 顶部提示文字
    var title: String by observable("雨渐大,30分钟后雨会停")
    
    // Y轴标签
    var rainLevel1Title: String by observable("暴雨")
    var rainLevel2Title: String by observable("大雨")
    var rainLevel3Title: String by observable("中雨")
    var rainLevel4Title: String by observable("小雨")
}

划重点by observable(...) 是 Kuikly 的响应式魔法!数据一变,UI 自动刷新,不用手动调用任何刷新方法。

3.2 定义组件:它长什么样?

kotlin 复制代码
class WeatherRainFallNodeCanvas : ComposeView<RainFallNodeAttr, ComposeEvent>() {
    private val nodeAttr = RainFallNodeAttr()

    override fun body(): ViewBuilder {
        val ctx = this
        return {
            attr {
                size(335f, 250f)  // 卡片大小
                borderRadius(BorderRectRadius(16f, 16f, 16f, 16f))  // 圆角
            }

            // 1. 毛玻璃背景
            Blur {
                attr {
                    absolutePosition(0f, 0f, 0f, 0f)
                    blurRadius(10.0f)
                }
            }

            // 2. 顶部提示文字
            Text { attr { text(ctx.nodeAttr.title) } }

            // 3. Canvas 画布(核心!)
            Canvas({ ... }) { context, width, height ->
                // 在这里画曲线
            }

            // 4. Y轴标签:暴雨、大雨...
            // 5. X轴标签:现在、30分钟...
        }
    }
    
    override fun createAttr() = nodeAttr
    override fun createEvent() = ComposeEvent()
}

第四步:用 Canvas 画曲线(核心!)

Canvas 就是一块"画布",你可以在上面画任何东西。

4.1 Canvas 基本用法

kotlin 复制代码
Canvas(
    { attr { absolutePosition(55f, 0f, 24f, 0f) } }
) { context, width, height ->
    // context = 画笔
    // width, height = 画布尺寸
}

4.2 画一条丝滑的曲线

直接用直线连接数据点会很丑(像心电图),我们用贝塞尔曲线让它丝滑:

kotlin 复制代码
// 1. 设置画笔
context.beginPath()
context.strokeStyle(Color(0x0085FF, 1f))  // 天空蓝
context.lineWidth(5.0f)                    // 线条粗细
context.lineCapRound()                     // 圆头端点
context.moveTo(0f, height)                 // 起点:左下角

// 2. 遍历数据点,画曲线
for ((index, node) in nodeAttr.nodes.withIndex()) {
    val x = index * width / 24                  // X坐标
    val y = height - node * height / 10         // Y坐标(数据映射)
    
    // 用贝塞尔曲线连接,比直线更丝滑
    context.quadraticCurveTo(controlX, controlY, nextX, nextY)
}

// 3. 完成绑定
context.stroke()

什么是贝塞尔曲线? 简单说:

  • 直线:A → B,走直线

  • 贝塞尔:A → B,但要"绕过"控制点 C,形成弧线

4.3 画虚线(参考线)

没有虚线 API?没关系,手搓一个:

kotlin 复制代码
private fun renderDashLine(context: CanvasContext, width: Float, y: Float) {
    context.beginPath()
    context.strokeStyle(Color(0xffffff, 0.2f))  // 半透明白
    
    var x = 10f
    while (x < width) {
        context.lineTo(x + 5f, y)   // 画 5 像素
        x += 7.5f
        context.moveTo(x, y)        // 跳过 2.5 像素
    }
    context.stroke()
}

原理:画一段、跳一段、画一段、跳一段...

第五步:让组件更好用

写个扩展函数,让自定义组件用起来和内置组件一样:

kotlin 复制代码
internal fun ViewContainer<*, *>.RainFallNodeCanvas(
    init: WeatherRainFallNodeCanvas.() -> Unit
) {
    addChild(WeatherRainFallNodeCanvas(), init)
}

现在可以这样用了:

kotlin 复制代码
RainFallNodeCanvas {
    attr {
        positionAbsolute()
        top(178.5f)
        left(20f)
    }
}

TextImage 一模一样的写法!

总结

恭喜完成 Kuikly "天气App"!回顾一下核心技能:

技能 总结
页面定义 继承 BasePager + @Page 注解
布局系统 attr { } 设样式,positionAbsolute() 绝对定位
自定义组件 继承 ComposeView,定义 Attr + body()
响应式数据 by observable() 数据变 → UI 自动刷新
Canvas bindbindbindbindbindbindbindind 回调式 API,拿到 context 就能画
贝塞尔曲线 quadraticCurveTo() 让曲线更丝滑
毛玻璃效果 Blur 组件一行搞定

下次产品经理甩给你"参考天气 App 做个图表"的需求,你就可以自信地说:小场面!

代码已经躺在 GitHub 上了,还不快去 Clone 下来跑一跑?

相关推荐
市场部需要一个软件开发岗位17 分钟前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
JMchen1232 小时前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
crmscs3 小时前
剪映永久解锁版/电脑版永久会员VIP/安卓SVIP手机永久版下载
android·智能手机·电脑
localbob3 小时前
杀戮尖塔 v6 MOD整合版(Slay the Spire)安卓+PC端免安装中文版分享 卡牌肉鸽神作!杀戮尖塔中文版,电脑和手机都能玩!杀戮尖塔.exe 杀戮尖塔.apk
android·杀戮尖塔apk·杀戮尖塔exe·游戏分享
机建狂魔3 小时前
手机秒变电影机:Blackmagic Camera + LUT滤镜包的专业级视频解决方案
android·拍照·摄影·lut滤镜·拍摄·摄像·录像
hudawei9963 小时前
flutter和Android动画的对比
android·flutter·动画
lxysbly5 小时前
md模拟器安卓版带金手指2026
android
儿歌八万首5 小时前
硬核春节:用 Compose 打造“赛博鞭炮”
android·kotlin·compose·春节
消失的旧时光-19438 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed
Jinkxs8 小时前
Gradle - 与Groovy/Kotlin DSL对比 构建脚本语言选择指南
android·开发语言·kotlin