ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenHarmony上Flutter自绘支出分析图表实践

OpenHarmony上Flutter自绘支出分析图表实践 1. 为什么我在OpenHarmony上坚持用Flutter做生活助手的支出图表在OpenHarmony上做生活助手App最先让我头疼的不是页面跳转也不是数据存储而是支出分析图表。生活助手这类带记账功能的应用“记一笔”只是入口“看懂钱去哪了”才是价值所在。我一开始想偷懒用WebView套一个可视化JS图表库结果离线状态下图表直接不渲染真机上字体和触摸反馈也跟原生页面脱节后来把所有图表全部改成Flutter绘制经过几个版本的迭代目前已经稳定跑在OpenHarmony设备上。这篇就把我在“Flutter for OpenHarmony生活助手App”里实现支出分析图表的完整过程拆开讲从数据口径到绘制方案从命中测试到真机踩坑基本是按照我当时实现的顺序来写。适合正在做OpenHarmony跨端应用、或者想在Flutter里自绘图表的朋友参考哪怕你只是打算给现有App加一个分类占比饼图里面关于聚合计算和绘制细节的部分也能直接用。1.1 生活助手场景下图表为什么是核心模块很多人做记账类功能需求清单里写着“增删改查账单”但真正用户天天打开看的其实是统计页。我调研过身边几个重度使用记账工具的朋友问他们最关注什么答案高度一致本月有没有花超、哪一类花钱最多、最近一周的消费是涨还是降。这些问题的答案表格能给但很费力图表才是把流水账变成结论的桥梁。在生活助手App里我把支出分析固定成首页默认Tab图表不是锦上添花而是核心路径。这也意味着图表不能慢、不能卡、不能加载半天更不能在切页的时候闪一片白屏。需求定了之后选型就变得很清晰我需要一个在OpenHarmony上渲染稳定、能离线工作、支持自定义交互的图表方案。1.2 Flutter for OpenHarmony的成熟度足够支撑业务落地坦白讲Flutter在OpenHarmony上的生态比Android和iOS要新不少但已经不是“跑通Hello World”的实验阶段。官方组织有一份长期维护的Flutter适配版本SDK渲染层走的是dart:ui大部分常用的Widget、手势、Canvas能力在OpenHarmony上都能正常工作。对我这个图表场景来说最爽的一点是图表绘制逻辑不依赖任何原生View也不依赖平台通道所以同一套图表代码可以直接复用在不同端上省掉三端各写一套图表的大量工作量。还有一点容易被忽略OpenHarmony系统的API风格与Android不完全一样如果你用原生方案做图表光是处理不同平台的生命周期、触摸事件、字体渲染就能耗掉不少时间。而Flutter自绘把这些统一到了Dart层听起来抽象但对开发者来说反而少了很多脏活。1.3 为什么图表必须在App内原生渲染而不是WebView壳现在很多跨端App喜欢在统计页套一个WebView加载一个开源图表库然后把JSON数据塞进去。这个方案在配置简单的展示场景下确实快但落到我自己这个项目里出现了几个问题首先是离线问题生活助手需要支持弱网甚至离线使用WebView里的图表库如果没缓存用户一断网就只能看到空白其次是触摸反馈WebView页面里的canvas触摸事件和Flutter页面之间总有那么一点“隔层毛玻璃”的感觉手感不对还有就是内存长列表加滚动图表WebView能吃到的内存比想象中大得多。原生图表路线也考虑过但OpenHarmony、Android、iOS三端分别算下来人力成本翻三倍。所以最终的路线没什么悬念Flutter绘制图表一次实现三端复用。下面我按实际开发顺序把每一个关键环节的决策和代码细节讲清楚。2. 环境搭建与工程初始化最容易卡住的三道坎2.1 版本匹配是第一道坎SDK不能随便用OpenHarmony上的Flutter开发第一步就和常规Flutter项目不一样你不能直接用官方flutter.dev下载的稳定版SDK而要用OpenHarmony社区维护的Flutter SDK fork。最开始我在这上面栽过跟头用普通Flutter SDK建工程模拟器能跑到了OpenHarmony设备上一编译就各种莫名其妙报错。正确的做法是到OpenHarmony的开源社区仓库拉取flutter_flutter和flutter_enginecheckout到和你目标版本对应的tag然后把bin目录加到PATH里。配置完之后用flutter doctor -v检查一下如果输出里能看到ohos相关的工具链信息说明环境基本就位。如果这里出现类似“the current configured flutter sdk is not known to be fully supported”的警告不用慌看清楚警告里提示的版本范围核对你的SDK tag与DevEco Studio、OpenHarmony SDK版本是否成组匹配。版本匹配这件事没有捷径唯一靠谱的方法就是以官方仓库README里给的版本组合表为准。git clone -b 对应release tag https://gitee.com/openharmony-sig/flutter_flutter.git export PATH$PATH:/path/to/flutter_flutter/bin flutter doctor -v2.2 建工程时要注意目录结构的变化带OpenHarmony支持的Flutter工程目录结构和普通Flutter工程不太一样。工程里多了一个ohos目录里面是一个完整的OpenHarmony工程有entry模块、配置文件、资源目录这些。创建命令大体是这样flutter create --platforms ohos expense_tracker。不同版本的命令写法有小差别如果你拿到的SDK版本比较老不支持直接指定ohos平台那就先创建一个标准Flutter工程再按SDK里README的说明跑适配脚本效果是一样的。创建完后你需要手动改几个地方一是应用包名默认生成的包名比较随机发布前一定要改成你自己应用的包名二是应用图标和名称在ohos目录的资源目录里替换三是权限声明生活助手需要读取存储之类的能力时在module的配置文件里加权限。图表本身不需要任何权限但如果你的App要导出账单、读取本地文件就得在这一步想清楚权限边界。2.3 真机运行容易被签名和hdc绊住OpenHarmony设备连接调试和Android稍微不同。Android用adbOpenHarmony用hdc两者命令行风格很接近但不能混用。设备上要提前打开开发者模式并开启USB调试然后用hdc list targets确认设备被识别。签名是我第一次跑真机时卡住最久的地方。OpenHarmony的安装包需要签名即使开发调试也绕不开。一开始我以为“debug包不需要签名”跑起来直接报install failed折腾半天才发现是签名配置缺失。解决办法是在DevEco Studio里配置自动签名或者手动生成签名文件并配置到工程里。真机上跑Flutter工程的命令是flutter run -d 首次构建时间会偏长如果中途卡住不要反复flutter run先构建出hap包用hdc install手动安装一遍能更清楚看到是哪一步出了问题。3. 支出图表的数据模型先算清楚再画清楚3.1 支出记录的数据结构不要敷衍图表画得再好底层数据一团糟也白搭。我的支出记录模型很简单但踩过坑之后才知道字段设计会影响聚合逻辑的复杂度。class ExpenseRecord { final String id; final double amount; final String category; final DateTime happenedAt; final String remark; }category一开始我用自由字符串结果用户随手输入“吃饭”“午饭”“美团外卖”统计时环形图冒出来几十个分类根本没法看。后来改成固定分类集合餐饮、交通、购物、居住、娱乐、医疗、其他录入时只能从这里选图表聚合才稳定下来。如果你要在自己项目里复用这个思路建议给category定义一个枚举存储层存枚举的字符串值UI层展示的时候再做脱敏处理。3.2 聚合逻辑按日、按周、按分类口径必须统一支出分析图表的本质是聚合把一堆流水记录按某种维度求和。我做了三个口径按日合计、按周合计、按分类合计。环形图用的是分类合计折线图用的是按日/按周合计。聚合时我用了一个通用方法核心逻辑就是遍历记录按key分组累加金额。有个容易踩的坑是日期处理如果用DateTime自带的toUtc做日期比较跨时区场景下某笔22点的消费可能被分到“第二天”导致图表里的日支出和你钱包里的数字对不上。我的处理是先把时间转换成本地日期的零点再作为keyfinal localDay DateTime(record.happenedAt.year, record.happenedAt.month, record.happenedAt.day);还有统计口径问题退款记录、负数金额、已作废的记录要不要计入我的做法是模型里加一个valid字段聚合时默认只统计valid为true的记录。这个字段别省否则用户退了一笔款图表里支出金额虚高一看就不对。3.3 数据到图形的映射业务坐标和画布坐标要分开刚开始画图时我有个坏毛病在CustomPainter里直接对List 做一堆循环计算边算边画。后来代码越来越难维护因为业务计算和绘制逻辑纠缠在一起。重构之后我明确分成两步先用数据层算出聚合结果得到一组“业务坐标”对象比如分类项名称、金额、占比再在UI层把这些对象映射成“画布坐标”——比如环形图里每个分类的起始角度、结束角度或者折线图里每个日期的Offset坐标。两套坐标系分离带来的好处是动画、命中测试、数据更新都变得很好写。动画只要在“业务坐标”到“画布坐标”之间加一个progress系数命中测试拿到的是画布坐标反向映射回“业务坐标”就能知道用户点的是哪个分类。4. 图表绘制方案选型第三方库还是CustomPaint自绘4.1 第三方图表库在OpenHarmony上的兼容性我实测过决定自绘之前我把主流的Flutter图表库都试了一遍。这里只说我实际感受到的差异给你做个参考。方案平台依赖OpenHarmony兼容性定制成本适用场景fl_chart纯Dart基础图表可用中高API封装较重快速接入标准柱状图/折线图syncfusion_flutter_charts纯Dart兼容性尚可高且涉及商业授权企业级复杂图表CustomPainter自绘纯Dart完全可控一次编写成本中等图表形态固定、交互定制多fl_chart在OpenHarmony上跑常规折线图、柱状图问题不大但它的API封装层级很深想改一个命中测试逻辑或者自定义动画要在它的内部结构里翻半天。syncfusion功能强但商用授权限制是个绕不开的点而且它的文档几乎没有提到OpenHarmony的适配细节。我的需求只有两种图表——分类占比环形图和每日趋势折线图形态非常固定所以最后毫不犹豫地选了CustomPainter自绘。4.2 环形图实现drawArc的细节决定了图表质感分类占比我用的是环形图也就是中间掏空的饼图。相比实心饼图环形图中心可以放大额数字视觉上更轻盈也方便放一个“查看全部”的跳转入口。绘制核心是canvas.drawArc。对每个分类先根据金额占比算出sweep角度然后按顺序累加起始角度画弧。这里有个小技巧用style设为stroke的圆弧来画而不是填充扇形。stroke可以方便地控制环的粗细还能让每个分类之间有自然的间距。double startAngle -pi / 2; // 从12点钟方向开始 for (var i 0; i items.length; i) { final sweep items[i].value / total * 2 * pi; final useSweep sweep - gap; // gap是扇区之间的间隔角 canvas.drawArc( Rect.fromCircle(center: center, radius: ringRadius), startAngle gap / 2, useSweep 0 ? 0 : useSweep, false, _ringPaint ..color items[i].color ..strokeWidth ringWidth, ); startAngle sweep; }环形图最关键的一个细节是gap的计算。如果每个扇区都减掉同样的gap总角度差会出现累积误差最后画出来的环在结束位置会缺一块或重叠一块。我兜底处理是最后一个扇区不减gap用它来吸收前面所有误差这样无论分类数量多少环都能闭合。4.3 趋势折线图增长曲线的绘制思路趋势图表我选了折线图展示最近14天或者最近30天的每日支出变化。实现思路不复杂先算出Y轴的最大值和最小值然后给上下各留10%的padding避免折线贴着画布边缘X轴按日期均匀分布每个日期对应一个Offset坐标点。画线的时候我用的是canvas.drawPath。把各点用Path连接起来注意数据点要去掉无效值比如某天没有记录就跳过不能让折线直接拉下来。每个节点画一个实心小圆点作为提示锚点极大值和极小值节点额外套一个浅色外圈这样用户一眼就能看出这周哪天“剁手”最狠。如果你想让曲线更平滑可以用三次贝塞尔连接相邻点但要注意过度平滑会掩盖真实波动。对支出分析来说直连折线更诚实所以我保留直连。这个取舍见仁见智但至少我测试下来普通用户扫码看直连和贝塞尔的观感差异并没有想象中那么大。5. 图表交互与性能优化点击高亮、动画与帧率调优5.1 环形图命中测试数学比API可靠图表画出来之后最常用的交互是点击某个分类查看这个分类下的金额、笔数、备注列表。命中测试的常规做法是给CustomPaint包一个GestureDetector在onTapDown的回调里拿到Offset然后自己做几何判断。环形图的命中测试分两步。第一步判断点击坐标是否落在环的范围内计算点与圆心的距离如果距离大于内半径且小于外半径才算点到环上。第二步是判断角度用atan2函数计算出Offset相对圆心的角度然后遍历所有分类的起始角度和结束角度找到包含该角度的分类。final dx offset.dx - center.dx; final dy offset.dy - center.dy; final distance sqrt(dx * dx dy * dy); if (distance innerRadius || distance outerRadius) return null; var angle atan2(dy, dx); if (angle 0) angle 2 * pi;命中测试用手写几何判断而不是依赖某个库的API是因为在这种自绘场景下CanvasEvent本身不携带“我点中了哪个对象”的信息所有识别逻辑都得自己算。这套算法写清楚之后后续要加长按、双击、Tooltip都只是在同一套几何判断上做扩展。折线图的命中测试就更简单我只判断点击位置与每个数据点的距离取最近且小于阈值的那一个基本满足需求。5.2 动画过渡让图表“长出来”而不是跳出来图表页进入的时候我不希望它“啪”地一下完整显示那样显得廉价。我的做法是加一个AnimationController进度从0到1驱动一个progress参数。环形图绘制时每个扇区的sweepAngle乘以progress折线图则用PathMetrics的extractPath按progress比例截取路径的一段实现“折线生长”的效果。动画时长控制在300毫秒左右太短了看不清太长了用户等得着急。曲线我用的是easeOutCubic先快后慢符合视觉习惯。这里说一个我踩过的坑动画开始时一定要先清理旧的Path对象和Paint对象否则连续快速切换日期范围会看到上一帧残影。5.3 刷新与性能RepaintBoundary和shouldRepaint图表页最容易出现的性能问题是“整个页面跟着重绘”。我用两个手段控制。第一个是RepaintBoundary。把图表组件包在RepaintBoundary里当页面其他部分刷新文字、列表时图表不会跟着重绘。第二个是CustomPainter的shouldRepaint方法。只在数据源真正变化时返回true比如切换了时间范围、更新了记录、窗口尺寸变化。如果只是父组件setState了一个和图表无关的变量painter发现新旧数据相同直接返回false跳过绘制。还有一个容易被忽略的点图表数据聚合不要放在build里。我最初在build方法里直接算summary页面一刷新就重复计算几万条记录的时候明显掉帧。后来把聚合逻辑挪到数据层并用compute放到Isolate里跑计算结束回到UI线程只有一串很小的图表数据对象主线程负担瞬间降下来。图表数据的变化频率没有用户手动记账那么频繁算完之后我还会做一层缓存同一天内重复进入图表页直接复用缓存结果不再重新遍历账单列表。6. 真机实测与踩坑记录OpenHarmony上独有的渲染问题6.1 字体缺失导致的文字错位与溢出图表在OpenHarmony真机上跑起来之后第一个让人挠头的问题是文字。开发时我在模拟器上看得好好的分类名称上了真机之后有的字间距变了有的直接变成豆腐块占位符。原因很明显目标设备少了某些系统字体TextPainter在layout阶段测量出的文本宽度和预期不一致导致我原本在画布里预留的文字位置全部错位。我的处理方案是把文字从画布里“搬”出来。画布只负责画图形分类名称、金额数字、百分比这些文本全部用Stack组件叠加在CustomPaint之上通过Positioned按计算好的位置对齐。这样一来文字渲染交给Flutter的文本排版引擎字体缺失时会自动回退不再依赖我手工计算的宽度。如果你的图表文字是固定的短标签也可以全部用文本组件而不是画布绘制。6.2 屏幕刷新率差异导致动画观感不稳定OpenHarmony设备的屏幕刷新率不统一有60Hz的也有高刷的。最开始我把动画时长定在200毫秒在开发板上看着流畅换到另一台高刷设备上却感觉“闪了一下”就结束了。后来统一把图表动画时长控制在300毫秒以上并且使用easeOut曲线中段速度放缓高刷设备上视觉停留时间更合适。图表动画本质上是锦上添花不要让动画速度成为用户在低端设备上的负担。6.3 内存与数据规模几万条记录下的聚合策略生活助手App用久了账单记录会积累到几万条甚至更多。图表页如果每次都全量加载记录再聚合内存和CPU都会吃不消。我的做法是数据层维护一个内存态的记录索引只加载当前统计周期需要的记录到一个轻量ListView里聚合结果缓存成Map。每次切周期只在缓存缺失时才去遍历否则直接取缓存把平均刷新耗时从几百毫秒降到了十几毫秒。这套策略在OpenHarmony开发板上实测稳定来回切换周/月视图也不卡。如果你要做更大量级的数据可以考虑直接在数据库层做SQL聚合把“数据准备”彻底下沉到存储层Dart侧拿到的已经是聚合结果。6.4 与其他原生模块的协作边界最后说一下图表和原生能力的边界。图表本身是纯Dart绘制不走平台通道所以在OpenHarmony上运行很干净。但生活助手除了图表还有导入账单、读取本地文件、系统日历同步这些功能这些绕不开MethodChannel或EventChannel。我的经验是所有原生通道调用都封装在一个独立的service层里图表UI永远不要直接调用MethodChannel。一方面是为了隔离通道通信在页面重建时有丢事件的风险另一方面也是让图表模块保持纯UI状态方便测试和跨端复用。以我实际踩坑的经历来说EventChannel的监听一定要绑定到页面生命周期。我在某个版本里把监听注册写在了initState结果页面销毁后没有取消订阅重复进入图表相关页面时事件回调触发了多次导致统计数字界面闪烁。对OpenHarmony上还在持续打磨的Flutter框架来说规范原生通道的使用边界比追求某个炫酷特效更重要。最后再分享一个实际操作中的小建议支出分析图表的配色尽量用色盲友好方案比如蓝、橙、绿、红这种差异明显的色系而不是一堆相近的渐变色。这不仅是照顾少数用户也让分类之间的对比更清晰。我自己第一次上线时用了整组青色渐变用户反馈“根本分不清哪个是餐饮哪个是交通”后来换了离散配色反馈立刻好转。图表做出来是给人看的清晰永远比好看优先。
返回列表