用 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 下来跑一跑?

相关推荐
QING6188 小时前
简单说下Kotlin 作用域函数中 apply 和 also 为什么不能空安全调用?
android·kotlin·android jetpack
城东米粉儿8 小时前
着色器 (Shader) 的基本概念和 GLSL 语法 笔记
android
儿歌八万首10 小时前
Jetpack Compose :封装 MVVM 框架
android·kotlin·compose
2501_9159214310 小时前
iOS App 中 SSL Pinning 场景下代理抓包失效的原因
android·网络协议·ios·小程序·uni-app·iphone·ssl
壮哥_icon10 小时前
Android 系统级 USB 存储检测的工程化实现(抗 ROM、抗广播丢失)
android·android-studio·android系统
Junerver10 小时前
积极拥抱AI,ComposeHooks让你更方便地使用AI
android·前端
城东米粉儿10 小时前
ColorMatrix色彩变换 笔记
android
方白羽10 小时前
告别onActivityResult:Android数据回传的三大痛点与终极方案
android·app·客户端
oMcLin10 小时前
如何在 RHEL 8 系统上实现高可用 MySQL 集群,保障电商平台的 24 小时稳定运行
android·mysql·adb