资讯动态

OpenHarmony 上 Flutter 饼图适配实战:从 fl_chart 空白页到自定义交互

发布时间:2026/10/20 3:01:38 来源:尧图企业网站定制
刚开始接触 OpenHarmony 应用开发时我接了一个挺常见的需求在数据看板里展示一张饼图。项目主体是 Flutter 写的图表库我直接选了 fl_chart——这库在 Android 上跑得丝般顺滑我原本以为换个平台无非就是重新编译一次的事。结果真把应用往 OpenHarmony 设备上一装页面刷出来饼图区域干干净净一片空白控制台倒是刷了一大摞红色日志。那天下午我排查了很久从 pubspec 依赖检查到日志逐行比对最后才意识到问题不在我的业务代码而在 Flutter 引擎本身。OpenHarmony 并不能直接跑标准 Flutter 的 Android embedder需要单独的分支和引擎产物。而 fl_chart 这种重度依赖 Canvas 绘制的第三方库恰好最容易踩到平台渲染差异的雷区。这篇博文就记录了我从零开始把 fl_chart 适配到 OpenHarmony、并实现一整套自定义饼图含点击交互、入场动画和视觉改造的完整过程包括遇到的坑和最终的性能优化方案。如果你也在做 OpenHarmony 上的 Flutter 图表开发或者正准备把一个依赖绘制的三方库搬过来这篇应该能帮你省下不少时间。1. 从一次空白页面说起fl_chart 为什么会在 OpenHarmony 上失灵1.1 场景还原当时我在做一个叫“模拟项目 X”的数据看板应用技术栈是 Flutter图表部分用了 fl_chart 的 PieChart。Android 模拟器、真机上跑得好好的换到 OpenHarmony 开发板后饼图整个区块不渲染。更诡异的是页面其他部分都正常说明 Flutter 应用本身是跑起来了只是图表控件出了问题。我第一反应是布局问题。顺手把饼图放进一个简单的 Container 里设了固定宽高结果还是一样。又把 fl_chart 版本从最新降回几个旧版本问题依旧。最后打开设备日志才看到类似这样的报错Unhandled Exception: Unsupported operation: drawArc with cornerRadius is not implemented on this engine.这才意识到问题出在渲染链路的底层。fl_chart 本身没有做任何平台判断它只依赖 Flutter SDK 暴露的 Canvas / Painting API。而这些 API 在 OpenHarmony 适配引擎上的实现完整度跟标准 Flutter 引擎并不完全一致。1.2 根因引擎分支与平台能力差异这里要先讲清楚一个背景OpenHarmony 不能直接复用 Android 那套 Flutter embedder。标准 Flutter 引擎通过 Android 的 Surface 和 OpenGL / Vulkan 来提供渲染能力而 OpenHarmony 有自己的图形栈和系统服务。要让 Flutter 在 OpenHarmony 上跑起来需要一个专门适配的 Flutter 分支以及配套的引擎产物。这个分支整体兼容性不错日常的 Widget、布局、网络基本没问题但凡是深入到 dart:ui 底层能力就容易出现差异。比如 Canvas 的 arc 相关绘制、Paint 的 shader、MaskFilter、还有手势事件上报的频率和坐标换算都可能跟标准引擎有细微出入。fl_chart 这种纯 Dart 绘制的图表库会把所有内容都压在 CustomPainter 的 paint 方法里完成所以它对底层 API 的触碰特别深。一旦 drawArc 或 Path 的某个重载没被实现整个图层就会静默失败。打个比方标准 Flutter 引擎是一套成熟的乐高说明书每个零件都严丝合缝OpenHarmony 适配分支是另一套兼容套件大颗粒度组件能装但个别小零件得自己打磨。fl_chart 恰好依赖了几个“小零件”。1.3 适配前要认清的目标在动手之前一定要先冷静评估你要的是“让 fl_chart 在 OpenHarmony 上跑起来”而不是“把 fl_chart 重写一遍”。所以整个适配工作的重心应该放在确认引擎分支对绘制 API 的支持情况给 fl_chart 和图表外层控件做一层隔离封装方便日后切换版本或引擎把 fl_chart 的能力边界摸清楚在 OpenHarmony 上能用什么、不能用什么提前规避。我建议文件结构上单独建一个charts/目录所有图表相关组件都放里面对外只暴露业务数据。这样即使某个图表库彻底不可用替换成本也被控制在了单个目录里。2. 搭建最小可运行环境Flutter 在 OpenHarmony 上的工程准备2.1 选对 Flutter 分支比写代码更重要这一步是整个适配的地基。我当时先从社区维护的 OpenHarmony Flutter 分支拉取了代码按 README 说明安装了配套的 Flutter SDK然后才在原有项目里接入。有几个关键点需要特别注意分支和引擎版本必须一一对应不能随便用标准 Flutter 的版本号去适配否则轻则编译失败重则运行时崩溃。初始化 OpenHarmony 平台目录。标准 Flutter 项目默认只有 android/ios/web 等目录要让应用跑在 OpenHarmony 上需要执行适配工具提供的平台初始化命令生成对应的工程目录。IDE 的选择。OpenHarmony 应用的构建和安装通常要在配套的 IDE 里完成命令行虽然也能跑通但对新手不够友好建议直接用 IDE 打开生成的工程目录操作。我这边用的适配分支属于比较新的版本工程初始化后会自动生成一个独立的平台工程目录。这个目录和 Flutter 主工程的关系是Flutter 负责写业务逻辑和 UI平台工程负责打包和调用系统能力两者通过产物联动。2.2 依赖引入与版本锁定确认环境没问题后接下来就是在项目里加 fl_chart。这一步比较简单pubspec.yaml 里加一行依赖即可dependencies: flutter: sdk: flutter fl_chart: ^0.68.0说个经验之谈跨端适配时不要盲目追最新版本。fl_chart 的版本迭代很快API 变动也比较频繁特别是 PieChart 相关的触摸回调、字段名在不同版本之间有差异。我建议先锁定一个已知能用的版本跑通之后再考虑升级。当时我试过 0.66、0.68 和更早的版本其中 0.68 系列在我用的 OpenHarmony 分支上表现最稳定。如果你的项目已经用了某个版本且跑得很正常那就继续用不要为了“新”而“新”。2.3 从最小 Demo 验证渲染链路依赖加好后先别急着做自定义效果而是先写一个最小 Demo验证 fl_chart 能否在 OpenHarmony 上完成最基本的绘制。这一步能帮你快速区分“库本身能不能跑”和“我的业务代码有没有踩坑”。import package:fl_chart/fl_chart.dart; import package:flutter/material.dart; class MinimalPieChartDemo extends StatelessWidget { const MinimalPieChartDemo({super.key}); override Widget build(BuildContext context) { return Scaffold( body: Center( child: SizedBox( width: 280, height: 280, child: PieChart( PieChartData( sectionsSpace: 2, centerSpaceRadius: 40, sections: [ PieChartSectionData(value: 40, color: const Color(0xFF6C5CE7), title: 40%), PieChartSectionData(value: 30, color: const Color(0xFF00B894), title: 30%), PieChartSectionData(value: 20, color: const Color(0xFFFDCB6E), title: 20%), PieChartSectionData(value: 10, color: const Color(0xFFE17055), title: 10%), ], ), ), ), ), ); } }如果这个最简单的饼图能在 OpenHarmony 设备上正常显示说明绘制链路是通的后面的自定义工作才有意义。如果连这个都是空白那就要回去检查引擎分支和运行环境而不是继续纠结代码。我当时跑通了这个 Demo 之后顺手记录了一下标准 Flutter 和 OpenHarmony 适配环境在调试上的几个差异方便后面少走弯路事项标准 FlutterAndroidOpenHarmony 适配环境热重载稳定改完即出经常失效需要冷重启应用设备日志adb logcat需要通过设备调试工具查看系统日志渲染异常多为布局或业务逻辑问题多了一层 engine API 兼容性问题部署流程直接 gradle 打包需要先在 Flutter 侧构建产物再到 IDE 打包安装3. 自定义饼图从数据模型到 Canvas 渲染3.1 PieChart 数据模型拆解最小 Demo 跑通之后就可以开始做真正的自定义了。但在此之前先把 fl_chart 饼图的数据模型搞清楚后面写代码才能有的放矢。PieChart 组件最核心的数据类是PieChartData常用字段如下字段作用个人备注sections扇区列表每个扇区一个PieChartSectionData数据源核心需要动态生成centerSpaceRadius中心圆洞半径调大就是环形饼图sectionsSpace相邻扇区之间的间隙调大能做出“花瓣”效果startDegreeOffset起始角度偏移默认从 0 度开始可以调到 -90 让第一个扇区朝上pieTouchData触摸交互配置点击高亮都靠它sectionsSpace扇区之间的视觉间隔想做分离式饼图的关键再往下拆PieChartSectionData才是真正控制“每个扇区长什么样”的类value数值决定了扇区占整个圆的比例color扇区颜色radius扇区绘制半径注意这个半径是“圆环带”的外半径而不是圆的半径title扇区显示的文本titleStyle文本样式titlePositionPercentageOffset文本距离圆心的百分比位置数值在 0~1 之间通常取 0.5~0.7 之间比较合适showTitle是否显示文本borderSide扇区描边可以通过描边颜色做出更多视觉效果badgeWidget在扇区上叠加自定义 Widget比如小图标、角标等这是个很实用的扩展点。3.2 底层绘制逻辑扇区如何变成图形理解了数据模型之后再看一下底层绘制逻辑。fl_chart 的饼图本质是一个 CustomPainter它在paint方法里对每个扇区做了这样几件事根据value占总量比例计算扇区的sweepAngle根据startDegreeOffset和前面扇区累加的角度算出当前扇区的起始角度用Canvas.drawArc或者 Path 绘制一个扇形环带——内半径由centerSpaceRadius决定外半径由PieChartSectionData.radius决定最后在扇区中心附近用TextPainter绘制文本或者叠加badgeWidget。所以这个图是“一层一层画出来的”每一帧都可以重绘。这也解释了为什么它对手势交互特别友好——你只需要在状态里改一下 radius 或颜色setState之后整张饼图就会被重新绘制。明白了这层原理你在自定义时就有了方向所有视觉变化本质都是“修改数据模型 → 触发重绘”的过程。比如想让选中扇区变大就把那个扇区的radius调大想让扇区之间有明显空隙就把sectionsSpace调大。3.3 三个自定义视觉效果的实践我这里分享三个我实际做过的自定义效果都是从业务需求出发的分别对应不同的视觉诉求。效果一环形饼图 中心统计文字这个在数据看板里特别常见。做法很简单一是把centerSpaceRadius调大让它变成一个圆环二是在 PieChart 外面叠一层 Stack把统计文字放在正中央。SizedBox( width: 280, height: 280, child: Stack( alignment: Alignment.center, children: [ PieChart( PieChartData( centerSpaceRadius: 64, sections: _buildSections(), ), ), const Column( mainAxisSize: MainAxisSize.min, children: [ Text(总参与人数, style: TextStyle(fontSize: 12, color: Colors.grey)), Text(1280, style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold)), ], ), ], ), )这里有个注意点外层 Container 需要固定尺寸PieChart 才会有一个确定的绘制区域。如果放在未约束的布局里CustomPaint 可能拿不到正确的宽高导致绘制被裁剪或者干脆不显示。效果二花瓣风格饼图所谓花瓣风格就是让每个扇区像花瓣一样中间有明显分隔。实现并不复杂关键在于两个参数的配合sectionsSpace调大到 4~8同时给每个扇区加上一个和背景色一致的粗描边描边宽度 2~4。这样扇区之间即使有重叠区域也会被描边遮住视觉上就像是独立的花瓣。PieChartSectionData( value: item.value, color: item.color, borderSide: const BorderSide(color: Colors.white, width: 3), radius: 90, )这种效果在 Android 上表现不错到 OpenHarmony 上也没有翻车因为 borderSide 本质上就是给路径描边不涉及复杂的高级渲染能力。如果你的主题背景不是纯白记得把borderSide的颜色改成背景色。效果三数据驱动的动态扇区真实的业务数据不会只有四个扇区更多时候是从接口拉回来的数组。我通常会写一个转换函数把业务对象映射成 fl_chart 的PieChartSectionData这样图表组件只依赖转换结果不会直接处理业务逻辑。class _PieItem { final String name; final double value; final Color color; const _PieItem(this.name, this.value, this.color); } ListPieChartSectionData _buildSections(List_PieItem items) { final total items.folddouble(0, (sum, item) sum item.value); return items.asMap().entries.map((entry) { final index entry.key; final item entry.value; final percent total 0 ? 0.0 : item.value / total * 100; return PieChartSectionData( value: item.value, color: item.color, radius: 80, title: ${percent.toStringAsFixed(0)}%, titleStyle: const TextStyle( fontSize: 13, color: Colors.white, fontWeight: FontWeight.w600, ), titlePositionPercentageOffset: 0.6, ); }).toList(); }注意这里titlePositionPercentageOffset一定要根据radius调整。默认 0.5 偏内如果 radius 很大文字可能会被裁剪到扇区边缘之外。我一般在 radius 70~90 时取 0.55~0.65。4. 让饼图活起来触摸交互与入场动画4.1 触摸回调的解析与状态复位静态饼图只是第一步真正让用户觉得“好用”的是点击扇区后的交互反馈。fl_chart 的触摸交互核心是PieTouchData用法如下PieTouchData( touchCallback: (event, response) { if (event is FlTapUpEvent) { final index response?.touchedSectionIndex ?? -1; if (index 0 index ! _touchedIndex) { setState(() { _touchedIndex index; }); } } else if (event is FlPanEndEvent) { setState(() { _touchedIndex -1; }); } }, handleTouch: true, )这里面有几个坑要提前说清楚event的类型有很多比如FlTapDownEvent、FlTapUpEvent、FlPanStartEvent、FlPanEndEvent等。一开始我只处理了FlTapDownEvent结果发现在手指按下瞬间就触发了高亮松开后如果回调里没有复位逻辑就会保持高亮状态。所以最合理的做法是按下/抬起时更新状态平移结束时复位。response可能是 null尤其是点击到中心圆洞或者扇区间隙时。代码里必须做空值判断不然会直接空指针崩溃。_touchedIndex初始值设成 -1表示“没有选中的扇区”这样不仅能用于高亮判断也能在动画复位时知道该恢复默认状态。4.2 点击扇区高亮弹出的实现有了触摸回调接下来就是把状态应用到扇区上。最直接的做法是选中的扇区 radius 调大颜色稍微调亮标题文本变粗。PieChartSectionData( value: item.value, color: item.color, radius: _touchedIndex index ? 96 : 80, title: ${percentValue}%, titleStyle: TextStyle( fontSize: _touchedIndex index ? 16 : 13, fontWeight: _touchedIndex index ? FontWeight.bold : FontWeight.w600, ), )注意这里_buildSections是在build方法里调用的每次setState都会重新计算结果列表。数据量小这么写没问题但如果扇区很多比如十几个以上建议把转换结果缓存起来后面性能部分我会细说。高亮弹出的效果在 OpenHarmony 上实测是稳定的因为 radius 的变化最终还是走 Canvas 绘制不涉及额外的平台通道。唯一要留意的是动画过渡——直接 setState 会让扇区“跳变”建议配合 Tween 做平滑过渡观感会好很多。4.3 入场生长动画的两种思路饼图加载时给个生长动画是整个自定义里观感提升最明显的操作。我试过两种方案各有适用场景。方案一用 fl_chart 自带动画能力新版本的 fl_chart 给 PieChart 提供了duration和curve参数。相当于数据变化时图表内部会自动做插值过渡。它的优点是省事缺点是某些 OpenHarmony 分支引擎上动画帧率不如预期而且对“扇区逐个出现”这种复杂节奏支持不够。方案二外层包 AnimationController 自己控制我最终选了方案二因为它在 OpenHarmony 上表现更可控。核心思路是用一个AnimationController控制 0~1 的进度然后让每个扇区的值都乘以进度late final AnimationController _controller AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), )..forward(); final animation CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic); AnimatedBuilder( animation: animation, builder: (context, child) { final progress animation.value; return PieChart( PieChartData( centerSpaceRadius: 64, sections: items.asMap().entries.map((entry) { final item entry.value; return PieChartSectionData( value: item.value * progress, color: item.color, radius: 80, title: _touchedIndex entry.key ? ${_percent(item.value)}% : , ); }).toList(), ), ); }, )这个方案的好处是动画进度可以做到 0 到 1 的平滑过渡扇区从无到有、从短到长自然生长而且progress可以被拦截配合点击高亮可以实现“点击时暂停动画”这类精细控制。有一点要注意value为 0 的扇区 fl_chart 不会绘制动画从 0 开始完全没问题。但如果你的业务数据里本身就有 value 为 0 的扇区它会被整段隐藏需要单独处理——比如强制设成最小值 0.01避免直接消失造成数据缺失的误读。5. 适配层的真正坎四个坑的完整排查记录5.1 drawArc 的 cornerRadius 能力缺失这个坑是让我最挠头的。前面提到的最小 Demo 能跑但一旦我给PieChartSectionData的borderSide加上圆角相关的配置或者底层绘制走到带cornerRadius的drawArc重载OpenHarmony 引擎就会直接抛异常。排查链路是这样的先确认最小 Demo 正常 → 怀疑问题出在 fl_chart 的扇形圆角特性 → 自己写了一个只调用Canvas.drawArc的测试页面 → 发现在引擎里确实不支持 cornerRadius 参数 → 最终锁定是适配引擎的绘制实现缺口。解决思路有两个如果只是个别绘制需求可以在自己的 CustomPainter 里规避圆角参数用 path 的弧线拼接出近似效果如果是 fl_chart 内部触发那就得换一个修复了该问题的引擎分支或者升级到更新的适配版本。我最终选择了换引擎分支因为 fl_chart 内部对圆角的支持是逐步在完善的靠外部 workaround 终究不是长久之计。5.2 手势命中坐标偏差与响应延迟第二个坑出现在点击高亮功能联调阶段。现象是点击扇区 A高亮的却是扇区 B而且偏差方向不稳定。排查时我先怀疑 PieTouchData 的命中计算有问题但同样的代码在 Android 上一切正常说明问题大概率在事件上报层。进一步排查发现OpenHarmony 适配引擎在部分版本上会把原始指针事件以物理像素坐标上报而 Flutter 侧期望的是逻辑像素坐标。如果设备 pixelRatio 不是 1.0命中计算就会整体偏移。解决方法优先升级引擎版本看是否修复了事件坐标转换如果暂时无法升级再考虑在事件回调里手动乘除MediaQuery.devicePixelRatioOf(context)做补偿。这里还有个连带问题事件响应偶尔有延迟特别是快速连续点击时第二个点击事件可能丢失。我的处理是不在回调里做复杂的逻辑比如网络请求只做状态更新同时把动画时长控制在 200ms 内避免交互被动画阻塞。5.3 字体、阴影与渐变的渲染差异饼图标题用的是TextPainter这块在 OpenHarmony 上也有讲究。默认字体加载路径和 Android 不同我遇到过一次标题文字全部变成方块的情况。原因是设备上没有匹配的 fallback 字体。解决方法是在应用启动时手动注册一个字体到 Flutter engine或者确保主题里的字体族在设备上存在。阴影和渐变同样要小心。fl_chart 的饼图默认对阴影支持有限如果自己通过 Paint 的MaskFilter.blur给扇区加阴影在 OpenHarmony 上执行效率很差甚至会导致绘制区域整体跳过。我的建议是避免在饼图上叠加阴影如果非要阴影用外层的 Container 做投影效果而不是在 Canvas 层做。5.4 热重载不可用带来的调试习惯调整这一点是纯效率问题。OpenHarmony 适配环境的热重载很不稳定改完代码经常提示 Dart VM 无法恢复执行。一开始我频繁按下热重载然后等了好几秒才发现没生效白白浪费时间。后来我养成了两个习惯修改涉及 CustomPainter、Canvas、状态初始化的代码时直接冷启动不做热重载只改文字、颜色、间距等静态配置时才偶尔尝试热重载一旦发现没生效就立刻切冷启动。这个习惯看起来不起眼但适配环境本来编译链路就比 Android 慢少走一次弯路就能省出很多时间。6. 性能验证与最终效果6.1 端上帧率与重绘范围功能都完成后我开始关注性能。饼图在静态显示时基本不消耗 CPU只要不 setState 就不会重绘。但在有入场动画和点击高亮时每帧都要重新绘制整张饼图扇区数量一旦变多帧率就会有波动。我的验证方法是用设备自带的性能分析工具抓帧率曲线重点观察两个场景一是入场动画播放期间二是快速连续点击扇区时。实测下来扇区数量在 6 个以内、不做额外阴影和模糊时OpenHarmony 真机上帧率稳定在 55 帧以上如果扇区超过 10 个同时标题文字较多帧率会明显下降。优化最直观的手段是给饼图区域包一层RepaintBoundary让它的重绘不要扩散到整棵树。这个改动非常小但效果立竿见影。RepaintBoundary( child: PieChart(...), )6.2 用数据预构建降低 setState 开销在交互逻辑里我一开始是每次build都调用_buildSections把所有业务数据重新映射一遍。这样写虽然简单但扇区多时会有不必要的对象创建和列表分配。优化思路是缓存。只要业务数据没有变化_buildSections的结果就能复用只有_touchedIndex或动画进度变化时才重建扇区列表。具体做法是把扇区列表拆成两部分基础配置颜色、数值、标题作为静态数据提前构建动态部分radius、标题样式在 setState 时单独覆盖。ListPieChartSectionData _cachedSections; void _rebuildSectionsIfNeeded(List_PieItem items) { if (_cachedItems ! items) { _cachedSections _buildSections(items); _cachedItems items; } }这套思路在 OpenHarmony 上同样适用因为性能瓶颈并不在于平台差异而在于 setState 触发的整树 rebuild。把重活挡在 build 之外自然就顺畅了。6.3 最终效果和几条总结性经验适配工作收尾时我在 OpenHarmony 真机上把整个流程完整跑了一遍接口拉数据 → 转换扇区 → 入场动画 → 点击高亮 → 动态切换数据源整体表现已经和 Android 端没有明显差距。最终项目交付后我把图表相关的代码整理成了项目内的通用组件后续其他界面复用饼图时只需要传数据和主题色完全不需要关心 fl_chart 或平台适配的细节。回顾整个过程有几条经验我觉得是对后来者最有价值的先把最小 Demo 跑通再谈自定义。任何图表异常先怀疑引擎和底层绘制不要一头扎进业务代码。fl_chart 版本要锁死升级要谨慎。不同版本对 OpenHarmony 引擎的兼容性差异可能很大。所有图表组件包一层隔离层对外只接收业务数据。这样引擎升级、库换新版本时你只需要改一个文件。阴影、模糊、渐变这一类高级绘制能力在 OpenHarmony 适配引擎上要尽量少用它们既是视觉降级重灾区也是性能热点。调试时尽量用冷启动代替热重载少一点期待就少一点浪费时间的沮丧。适配 OpenHarmony 这件事本质上是在“标准 Flutter 能力”和“平台差异化实现”之间找平衡。fl_chart 用得好不好取决于你对它的数据模型和绘制原理理解得透不透能不能在 OpenHarmony 上跑得顺取决于你对引擎边界摸得清不清。这篇文章里写到的坑、排查过程和优化手段都是我这边真实遇到过、解决过的。如果你照着步骤走至少能绕开我踩过的那几个大坑直接把精力花在饼图的视觉和交互打磨上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑