用 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)
    }
}

和 Text、Image 一模一样的写法!

总结

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

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

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

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

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui