当前位置: 首页 > news >正文

鸿蒙开发入门指南:鸿蒙canvas实操——快速掌握自定义图表组件

鸿蒙开发入门指南:鸿蒙canvas实操——快速掌握自定义图表组件

    • 一、为什么需要自己画图表?
    • 二、自定义图表引擎:CanvasController 的核心实现
      • 2.1 把数据转换成坐标点
      • 2.2 贝塞尔曲线平滑连接
      • 2.3 绘制点和标签
    • 三、完成基础天气卡片:列表 + 曲线 + 交互
    • 四、15天预报卡片:双曲线叠加
    • 总结

大家好,我是木斯佳。今天聊一个实际的问题:在鸿蒙上想画个折线图,结果发现图表库少得可怜。不像前端有 ECharts、AntV、Highcharts 随便挑,如果有自定义需求,鸿蒙生态在这块基本靠自力更生。

那怎么办?自己画呗。正好我最近研究了一个鸿蒙天气组件的实现,里面有一套非常完整的 Canvas 曲线绘制方案。这篇文章就拆开给大家看看,怎么从零到一封装一个可复用的天气图表组件。

一、为什么需要自己画图表?

先说说背景。目前鸿蒙生态里专门做图表的库确实不多,就算有,也不一定适配你的业务场景。比如天气应用里的温度曲线,需求其实挺明确的:

  • 横轴是时间(小时/天)
  • 纵轴是温度值
  • 需要平滑曲线
  • 支持点击显示具体数值
  • 多曲线叠加(最高温/最低温)

这种需求,与其找第三方库凑合,不如自己封装一套。而且封装好了,空气质量趋势、股票行情、用电数据都能复用。

在开始写代码之前,先理清楚架构。这个天气组件把图表能力分成了三层:

业务页面(Home) ↓ 图表组件(UIHours / UIDays) ↓ 绘图引擎(CustomCanvas + CanvasController)
  • 业务页面:只管传入数据和布局
  • 图表组件:负责组装列表和曲线,处理点击交互
  • 绘图引擎:负责所有 Canvas 绘制逻辑(坐标转换、贝塞尔曲线、标签渲染)

这样分层的好处是:绘图引擎写好之后,24小时天气和15天预报都能复用,不用写两遍绘制代码。

二、自定义图表引擎:CanvasController 的核心实现

先看最底层的CanvasController,它是整个图表能力的核心。

2.1 把数据转换成坐标点

温度数据是数值,画到 Canvas 上需要转成坐标。核心算法如下:

publicgenerateTemperaturePoints(temperatureArray:number[],intervalLength:number,// 每个点的间距canvasHeight:number,minTemperature:number,maxTemperature:number,):PointWithTemp[]{constpoints:PointWithTemp[]=[];consttemperatureRange=maxTemperature-minTemperature;for(leti=0;i<temperatureArray.length;i++){// X 坐标:间隔中点位置constx=intervalLength/2+i*intervalLength;// Y 坐标:根据温度值映射到 Canvas 高度constnormalizedValue=(temperatureArray[i]-minTemperature)/temperatureRange;consty=canvasHeight-normalizedValue*canvasHeight;points.push({x,y,temp:temperatureArray[i]});}returnpoints;}

逻辑不复杂:X 轴等距分布,Y 轴按温度比例计算。minTemperaturemaxTemperature是为了保证不同温度范围都能正确映射到 Canvas 高度上。

2.2 贝塞尔曲线平滑连接

如果直接把点连成折线,效果太生硬。这里用了贝塞尔曲线做平滑处理:

private_drawCurve(ctx:CanvasRenderingContext2D,temps:PointWithTemp[]):void{// 计算贝塞尔控制点constbezierControls=this._calcBezierControls(extendedPoints[i-1],extendedPoints[i],extendedPoints[i+1],extendedPoints[i+2],);ctx.bezierCurveTo(bezierControls.C1.x,bezierControls.C1.y,bezierControls.C2.x,bezierControls.C2.y,extendedPoints[i+1].x,extendedPoints[i+1].y,);}

贝塞尔曲线的核心思想是:用前后两个点的位置来计算当前段的控制点,让曲线经过每个数据点,同时保持平滑。具体算法这里不展开,感兴趣的可以去看_calcBezierControls的实现。

2.3 绘制点和标签

曲线画完之后,还要在每个数据点上画圆点,并标出温度值:

private_drawPointsAndLabels(ctx:CanvasRenderingContext2D,temps:PointWithTemp[]):void{for(constpointoftemps){// 画圆点ctx.beginPath();ctx.arc(point.x,point.y,this._holeRadius,0,Math.PI*2);ctx.fill();// 画标签if(this._labelPosition!=='none'){constlabelText=Math.round(point.temp)+'°';constlabelY=this._labelPosition==='top'?point.y-20:point.y+20;ctx.fillText(labelText,point.x,labelY);}}}

labelPosition参数很实用:最高温曲线标签在上方,最低温在下方,避免重叠。

三、完成基础天气卡片:列表 + 曲线 + 交互

CanvasController只管绘图逻辑,那谁来触发绘制?答案是CustomCanvas组件:

@ComponentV2exportstruct CustomCanvas{@Require@Paramcontroller:CanvasController;@Require@Paramlist:number[];@Paramindex:number=0;privatecontext:CanvasRenderingContext2D|null=null;@Monitor('list')drawAgainWhenListChange(){this.points=this.controller.generateTemperaturePoints(this.list,this.itemWidth,this.canvasHeight,this.min,this.max);this.controller.draw(this.context!,this.points,this.index);}@Monitor('index')drawAgainWhenIndexChange(){this.controller.draw(this.context!,this.points,this.index);}build(){Canvas(this.context).width('100%').height(this.canvasHeight).onReady(()=>{this.context=this.context;this.drawAgainWhenListChange();})}}

这个组件做了三件事:

  1. 监听list变化,数据变了就重新计算坐标点并重绘
  2. 监听index变化,选中项变了就重绘高亮效果
  3. 通过@Monitor装饰器自动响应变化,不用手动调用

前端视角:这有点像 React 的useEffect,依赖变了就重新执行。

有了底层绘图能力,上层组件就好写了。UIHours的布局很有意思:

Scroll(){Stack(){// 1. 温度曲线(底层)CustomCanvas({controller:this.controller,list:this.temps,max:this.maxTemp,min:this.minTemp,index:this.curIndex,})// 2. 列表覆盖层(上层)List(){ForEach(this.weathers,(item,index)=>{this.itemBuilder(index)})}// 3. 选中提示气泡if(this.showText){Text(this.showText).position({left:this.curIndex*this.itemWidth,top:曲线位置计算,})}}}

注意这里的层级:曲线在最底下,列表在上层透明覆盖。用户点击列表的某一项时,更新curIndex,然后:

  • 曲线重新绘制,高亮对应的数据点
  • 气泡移动到对应位置
  • 列表项本身也高亮

这种「数据驱动 + 分层绘制」的方式,比把所有逻辑塞在一起清晰得多。

四、15天预报卡片:双曲线叠加

15天预报比24小时复杂一点,因为要同时显示最高温和最低温两条曲线。

实现方式很简单:叠两个CustomCanvas

Stack(){// 最高温曲线(标签在上方)CustomCanvas({controller:this.maxController,list:this.maxTemps,labelPosition:'top',curveColor:'#FBB460'})// 最低温曲线(标签在下方)CustomCanvas({controller:this.minController,list:this.minTemps,labelPosition:'bottom',curveColor:'#3F7EF7'})// 列表覆盖层List(){...}}

两个 Canvas 共用同一套 X 轴坐标,通过不同的labelPositioncurveColor区分。第一列「昨天」还用虚线做了视觉弱化,细节处理得不错。

所有天气卡片都包在CardContainer里:

@ComponentV2exportstruct CardContainer{@Paramtitle:string='';@BuilderParamcontent:()=>void;build(){Column(){Text(this.title).fontSize(18).fontWeight(FontWeight.Bold).height(56).margin({left:16})if(this.content){this.content()}}.width('100%').backgroundColor($r('sys.color.comp_background_primary'))}}

使用的时候:

CardContainer({title:'24小时天气预报'}){UIHours({weathers:this.vm.hourlyWeathers})}

这个小封装虽然简单,但保证了所有卡片标题样式一致,后续加新卡片也只需套一层。

数据变化时,图表要自动重绘。这个组件通过HomeVM来管理状态:

@ObservedV2exportclassHomeVM{@TracehourlyWeathers:HourlyWeather[]=[];@TracedailyWeathers:DailyWeather[]=[];@Monitor('location')private_change(){WeatherUtils.getHourlyWeathers(this.location.code).then(res=>this.hourlyWeathers=res);WeatherUtils.getDailyWeathers(this.location.code).then(res=>this.dailyWeathers=res);}}

@Monitor装饰器监听location变化,位置变了就重新拉数据。数据更新后,@Trace会自动触发 UI 刷新,CustomCanvas里的@Monitor('list')又会自动重绘曲线。

整个链路是:位置变化 → 数据拉取 → 列表更新 → 曲线重绘,全程自动化。
实际开发中需要注意:

1、CanvasController配置不同的颜色、标签位置、虚线样式,就能适配 24 小时和 15 天两种场景,不用写两遍。
2、选中项、数据列表、曲线重绘全部通过状态变化触发,不需要手动调用绘制方法。
3、鸿蒙的@Monitor装饰器非常实用——依赖变了就自动执行逻辑,省去手动调用的麻烦。

总结

这个天气组件的实现给了我们一个很好的范本:

  • CanvasController封装绘图算法(坐标转换、贝塞尔曲线、标签渲染)
  • CustomCanvas桥接数据和 Canvas 生命周期
  • UIHours/UIDays组装列表和曲线,处理交互
  • CardContainer统一视觉风格

这套分层不止能做天气曲线,空气质量趋势、股价走势、用电数据监控,换换数据和样式就能复用。

如果你正在鸿蒙上做类似的可视化需求,不妨参考这个思路:把绘图能力抽成独立引擎,上层组件只负责数据映射和交互。这样既解决了图表库缺失的问题,也给自己攒了一套可复用的轮子。

http://www.jsqmd.com/news/672784/

相关文章:

  • Sqoop和DataX到底怎么选?从我们的数仓迁移实战聊聊工具选型
  • 保姆级教程:用YOLOv11+PyQt5做个垃圾分类小助手(附完整代码和数据集)
  • Obsidian Weread插件:一键同步微信读书笔记到知识库的高效解决方案
  • MAA明日方舟自动化助手:从零开始的全功能使用指南
  • 田纳西男子多次黑入美国最高法院文件系统:安全防护与访问控制剖析
  • 别再折腾WSL2了!Windows 10/11一键搞定Docker Desktop安装(附保姆级排错指南)
  • 别再调参了!用KELM(核极限学习机)做回归预测,Matlab代码实战与性能对比
  • 免费解锁iPhone激活锁:使用applera1n工具完整指南
  • 终极免费卡拉OK游戏:UltraStar Deluxe完整入门指南
  • Golang怎么设置响应状态码_Golang如何用WriteHeader返回404或500状态【基础】
  • 如何用BabelDOC轻松解决PDF翻译难题:5步完整指南
  • VSCode调试Python时,Step Into/Over/Out到底怎么选?一张图讲清楚
  • 从CAD老手到中望3D新手:快速上手的草图绘制习惯迁移与效率技巧
  • 避坑指南:ESP32串口通信(UART)那些让人头大的报错,我都帮你解决了
  • 技术深度解析:League Akari如何重新定义英雄联盟自动化工具
  • MIL-53(Al)修饰四氧化三铁纳米颗粒,MIL-53(Al)@Fe₃O₄ NPs,反应机制
  • 3步诊断与彻底解决Joplin多设备同步冲突的完整指南
  • 告别Tesseract-OCR配置玄学:一份给OpenCV/Pytesseract用户的避坑清单与终极配置指南
  • 别再只用箱线图了!用R的Raincloud Plots(云雨图)可视化你的纵向数据,附完整代码
  • 从工艺到特性:基于Silvaco Athena/Atlas的BJT设计与仿真全流程解析
  • Windows Cleaner:三招拯救你的C盘,让Windows系统重获新生
  • 告别抓瞎调试!用SocketTools这款TCP/UDP测试工具,5分钟搞定网络通信自测
  • 从IPC标准到电路实测:PCB板材Dk/Df测试方法的选择与权衡
  • 在亚马逊云EC2上部署MacOS实例:从专属主机配置到远程桌面连接全攻略
  • 告别串口占用!用JLink RTT Viewer调试NRF52832蓝牙项目(附完整SDK配置流程)
  • 2026实战:LangChain智能体无缝部署到OpenClaw集群,5分钟完成生产级上线
  • nanobot保姆级教程:Qwen3-4B tokenizer分词结果可视化、special token作用解析
  • Jetson Nano/Xavier设备树修改避坑指南:从反编译到源码编译的两种实战方法
  • FutureRestore GUI终极指南:图形化iOS固件恢复深度解析
  • SSH 免密登录与 config 配置