资讯详情 Flutter图表库fl_chart适配OpenHarmony:自定义饼图完整实践
📅 2026/10/18 23:15:23
折腾了大半个周末我终于把 fl_chart 这个 Flutter 图表库完整跑到了 OpenHarmony 上并且用它的 PieChart 组件实现了一个带中心文案、支持点击交互的自定义饼图。整个过程没有想象中那么恐怖但绝对算不上一条命令就能跑通。这篇文章把我从踩坑到跑通的完整路径写下来为什么选它、适配前要准备什么、实际改了哪里以及最后怎么用 fl_chart 在这个平台上做出一个像样的自定义饼图。如果你也在计划把 Flutter 应用落到 OpenHarmony 上同时又需要图表类功能这篇内容应该能帮你少走不少弯路。坦白说最开始我并没有把它当成一个“适配项目”来做只是想在一个已有的 Flutter 业务工程里加一个财务报表用的饼图。结果一编译到 OpenHarmony 目标设备白屏、字体告警、触摸无响应全来了。后来我才意识到图表库能不能在 OpenHarmony 上跑和它是不是“纯 Dart”实现有直接关系。下面我把整个思考过程和实操细节一步步展开。1. 为什么偏偏是 fl_chart1.1 Flutter 应用在 OpenHarmony 上到底卡在哪Flutter 本身是一套跨端 UI 框架一套 Dart 代码可以在 Android、iOS、Web 和桌面端跑。但 OpenHarmony 并不是 Flutter 官方直接支持的目标平台想在 OpenHarmony 上运行 Flutter 应用通常要依靠社区或厂商维护的 Flutter 引擎适配版。这意味着绝大多数 Flutter 三方库能不能直接跑关键就看它到底依赖了什么东西。常见的失败原因并不是 Dart 代码有问题而是三方库内部调用了 Android 或 iOS 独有的原生能力也就是 Platform Channel 那一套。比如定位、相机、生物识别这类库底层必须要走原生插件而 OpenHarmony 侧如果没有人写对应的原生实现编译期或运行期就会直接挂掉。但图表库不一样。像 fl_chart 这类纯 Dart 方案它的所有绘制都是通过 Flutter 引擎提供的 dart:ui 能力完成的包括 Canvas、Paint、TextPainter 这些不依赖任何原生 View也不走平台通道。所以理论上只要 Flutter 引擎本身能在 OpenHarmony 上跑fl_chart 就有机会跑起来。真正的难点不在“移植库本身”而在于处理 OpenHarmony 版本引擎的细节差异。1.2 为什么不用自绘或其它图表库决定用 fl_chart 之前我也考虑过三个方案直接用 CustomPaint 自绘饼图、使用另一个图表库、用 fl_chart。自绘饼图听起来很简单画几个扇形加几条线就够了。但一旦你要处理动画、点击命中、文字防重叠、自适应屏幕宽度这些需求自绘方案的工作量会迅速膨胀而且后面每次改交互都需要自己维护实在不划算。表格对比一下我当时做的选型分析方案适配 OpenHarmony 的难度功能完整度长期维护成本fl_chart较低纯 Dart 实现主要是引擎差异处理高饼图/柱状图/折线图/散点图都有低社区活跃另一个老牌图表库中等部分组件依赖旧版动画机制中更新节奏慢中偏高自绘 CustomPaint完全可控但底层能力全部自己搭低只适合极简需求高fl_chart 还有一个很实际的优势官方文档里的示例足够丰富饼图的 API 分了数据、交互、动画三个维度很多需求直接用参数就能配出来不需要自己造轮子。另外它在 Flutter 官方生态里更新比较频繁bug 修复也快。相比之下自绘方案虽然“没有依赖”但其实是把成本从选型阶段转移到了长期维护阶段。对我这种需要快速交付图表模块的开发者来说fl_chart 是最稳妥的选择。2. 适配前的准备工作与工程初始化2.1 环境版本、工程结构和第一份 pubspec在动手写饼图之前先把基础环境梳理清楚。我当时的开发机装的是 DevEco Studio配套的 SDK 和开发工具链都是按 OpenHarmony 应用开发的标准流程来的。与此同时还要准备一个能在 OpenHarmony 上运行的 Flutter 引擎版本这个版本通常会滞后于 Flutter 官方主分支所以第一步就是确认 Dart SDK 版本和 Flutter 引擎版本分别是什么。版本对齐特别重要。我自己第一次踩的坑就是本机 Flutter 用的是较新的稳定版而 OpenHarmony 适配引擎是基于旧一点的 Flutter 版本做的编译的时候一大堆 API 找不到日志刷得人头皮发麻。所以建议你在创建工程之前先查清楚 OpenHarmony 适配引擎对应的是哪个 Flutter 版本、哪个 Dart 版本然后把本机和项目都切到一致。工程初始化完成后第一件事是在pubspec.yaml里加上 fl_chart 依赖dependencies: flutter: sdk: flutter fl_chart: ^0.66.0然后执行flutter pub get。如果你的网络环境比较特殊或者公司内部有统一依赖管理平台拉取公共源可能会失败。这时候有两种常见替代方案一种是把 fl_chart 的源码放到本地目录通过path方式引入另一种是把依赖包缓存到离线目录每次构建直接指向缓存。两者的差别在团队协作场景下会比较明显用固定版本号加 lockfile 的方式可复现性更强而用本地路径更容易被业务临时改到适合验证阶段。2.2 先跑通一个最小可运行工程别急着画图这一步是很多教程不会强调但我强烈建议不要跳过的环节。先不要碰 fl_chart也先不要写任何业务图表直接在 OpenHarmony 设备或模拟器上跑一个最简单的 Flutter 页面比如一个只有文本和按钮的计数器。如果这个默认页面能正常显示说明 Flutter 引擎和 OpenHarmony 原生壳之间的通路是通的后续问题才可以放心归因到图表库和引擎差异上。我当时把这个验证页称为“适配基座”后面所有实验都在这份代码上做增量。基座里只保留了三样东西Flutter 入口、一个渲染文本的页面、一个按钮触发页面刷新。这样做的价值在于当饼图出现异常时我可以快速判断是引擎的基础渲染问题还是 fl_chart 自身的问题不会把时间浪费在模糊的“哪里都可能有问题”的排查上。如果这个最简页面都跑不起来通常说明问题在 Flutter 引擎构建层或项目配置层这时候请先解决基线问题再引入图表库。3. 从移植到跑通fl_chart 适配的完整过程3.1 第一次运行官方饼图样例时的场面我把基座跑通之后迫不及待地把 fl_chart 官方示例里的饼图代码粘了进去目标设备是一台 OpenHarmony 模拟器。结果页面冷启动之后白屏了控制台里并没有崩溃但不断有字体相关的警告刷出来。这个现象非常有迷惑性因为它没有明确报错只是图表不出现。我一度以为是数据没传进去后来才发现问题根本不在数据。经过反复试最终定位到根因OpenHarmony 上的 Flutter 引擎由于字体回退表与 Android 不完全一致TextPainter在测量带有中文的文本时返回的宽度可能为 0 或异常值导致 fl_chart 内部计算每个扇区宽度时得到空值最终整个饼图就没有渲染出来。这个坑相当隐蔽因为你从 Dart 代码上看数据、颜色、半径全部都设置对了但底层测量已经出了问题。另一个常见报错是构建阶段提示画布相关初始化失败或者找不到某个渲染后端符号。这类问题通常是 Flutter 引擎切换渲染后端导致的比如 OpenHarmony 适配版默认走软件渲染而 fl_chart 内部部分绘制逻辑调用了 GPU 光栅化才有的接口。遇到这种情况第一步不是去改 fl_chart而是确认引擎是否提供了软件渲染回退开关。3.2 关键差异点字体、Canvas 和触摸映射把跑通逻辑梳理开之后fl_chart 适配 OpenHarmony 的核心差异主要集中在三块。第一块是字体测量。刚才说的白屏问题我的处理方式是在 Flutter 全局 theme 里显式指定字体回退列表把中文字体路径加进fontFamilyFallback。这样TextPainter在测量的时候就不会因为找不到字形而返回异常宽度。对于饼图这种大量依赖文本测量来确定扇区标题位置和中心文字布局的场景字体配置直接影响渲染正确性。第二块是 Canvas 的兼容性。saveLayer、阴影、渐变这类绘制操作在软件渲染和 GPU 渲染两种模式下表现差异很大。我在真机上曾经看到过饼图扇区带渐变后出现一块块色斑模拟器上却完全正常。后来定位到是因为渐变绘制走了某个 GPU 特性而 OpenHarmony 适配引擎在特定驱动下没有正确回退。我的解决方案是比较保守的在真机运行的版本里暂时关掉阴影改用纯色加扇形边缘分隔线来保持视觉层次。这样做虽然牺牲了一点立体感但换来了跨设备的一致性数据看板本身要的是准确而不是花哨。第三块是触摸事件映射。fl_chart 的交互依赖 Flutter 的手势事件而 OpenHarmony 设备上输入事件要先经过 Flutter 引擎的事件桥接再透传给 Dart 层。实际测试下来基础点击事件基本可用但如果在不同屏幕密度设备之间切换命中区域可能会有几个像素的偏差。处理办法是在使用触摸回调时加上MediaQuery.devicePixelRatio对命中坐标做一次校准或者放宽点击命中范围。3.3 用最小数据集完成第一版可用适配在适配过程中我没有一次性把所有功能都堆上去而是准备了一个“最小数据集”只有三个扇区颜色各不相同没有动画没有交互没有渐变。先把这三段数据在 OpenHarmony 上稳定渲染出来再逐步把中心文字、渐变、点击效果加回去。这种增量式的做法非常有效。每加一个功能如果出了问题我都能明确判断是这一步改动引入的而不是前面已经积累了几十个变量。大约用了一下午我就把 fl_chart 官方饼图样例完整跑通了。需要注意的一点是官方样例里如果是用 GoogleFonts 加载远程字体的在 OpenHarmony 开发阶段不要直接搬进来因为离线或者网络受限环境下字体加载会直接导致页面闪退。把网络字体替换成本地字体是更稳妥的路线。4. 用 fl_chart 实现自定义饼图4.1 先搭出饼图骨架适配跑通之后真正有意思的部分才开始。我需要的不只是官方示例那种普通环形图而是要一个能放进业务看板里的自定义饼图中空圆环、每个扇区独立颜色、点击某个扇区后中心文字联动更新。fl_chart 的 PieChart 组件对这种需求支持得很好核心数据结构就是PieChartData包一层扇区列表。先看基础骨架代码import package:flutter/material.dart; import package:fl_chart/fl_chart.dart; class CustomPieChartPage extends StatefulWidget { const CustomPieChartPage({super.key}); override StateCustomPieChartPage createState() _CustomPieChartPageState(); } class _CustomPieChartPageState extends StateCustomPieChartPage { final List_PieItem _items [ _PieItem(渠道A, 30, const Color(0xFF4FC3F7)), _PieItem(渠道B, 25, const Color(0xFFFFB74D)), _PieItem(渠道C, 20, const Color(0xFF81C784)), _PieItem(渠道D, 15, const Color(0xFFE57373)), _PieItem(其它, 10, const Color(0xFF9575CD)), ]; override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(自定义饼图)), body: Padding( padding: const EdgeInsets.all(16), child: PieChart( PieChartData( startDegreeOffset: -90, centerSpaceRadius: 70, sectionsSpace: 3, sections: _items .map( (item) PieChartSectionData( value: item.value, color: item.color, radius: 68, title: ${item.value}%, titlePositionPercentageOffset: 0.6, titleStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white, ), ), ) .toList(), ), ), ), ); } } class _PieItem { const _PieItem(this.label, this.value, this.color); final String label; final double value; final Color color; }这里几个参数需要重点解释。startDegreeOffset控制饼图起始角度设为 -90 后第一块数据从 12 点方向开始符合大多数看板的阅读习惯。centerSpaceRadius是中空圆环的半径值越大中心空白区域越大给中心文字留出空间。sectionsSpace是扇区之间的间隙设为 2 到 3 会让饼图有分隔感看起来更精致。titlePositionPercentageOffset控制扇区标题离中心的距离0.6 表示从中心到边缘这条半径上 60% 的位置太大会导致文字溢出扇区太小又会和中心文字重叠。4.2 让饼图长得像产品想要的样子骨架完成后自定义样式主要集中在几个方向。中心文字是产品侧比较关注的功能但 fl_chart 本身并不直接在饼图中心渲染文案常规做法是用 Stack 把饼图和中心文本叠起来。因为centerSpaceRadius已经预留了中心空白区只要把文本放在 Stack 的中心视觉上就像饼图的一部分。这种方法实现简单还便于后面做点击联动。扇区颜色方面我建议不要在PieChartSectionData里反复写死颜色而是像示例代码一样把每个数据项的颜色集中定义在数据模型里。这样后续如果要做主题切换只要替换数据模型里的颜色值即可不需要碰布局和图表配置。渐变和阴影需要谨慎。fl_chart 的PieChartSectionData提供了gradient属性但如果目标是 OpenHarmony 真机一定要先在目标机型上验证。刚才提到的色斑问题就是因为渐变和阴影叠加触发了不同的渲染路径。我的做法是把渐变做成可选开关默认关闭只有在确认设备渲染正常时才打开。扇区上还可以加badgeWidget这个属性允许你在扇区外侧放一个自定义小组件比如带阴影的小圆点或者数值标签。我在模拟项目X里用它在关键扇区外侧放了一个“12%”的增长箭头效果比普通文本标题直观很多。注意badgeWidget的使用要控制数量扇区太多时有遮挡风险。4.3 交互和动画点击联动中心文字自定义饼图最核心的交互是点击扇区后更新选中的状态。fl_chart 处理这一步很方便只需要在PieTouchData里注册回调然后通过setState更新当前点击的索引即可。完整交互代码class _CustomPieChartPageState extends StateCustomPieChartPage { int _touchedIndex -1; String _centerLabel 总计; double _centerValue 100; override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(自定义饼图)), body: Stack( alignment: Alignment.center, children: [ Padding( padding: const EdgeInsets.all(16), child: PieChart( PieChartData( startDegreeOffset: -90, centerSpaceRadius: 70, sectionsSpace: 3, swapAnimationDuration: const Duration(milliseconds: 400), swapAnimationCurve: Curves.easeOutCubic, pieTouchData: PieTouchData( touchCallback: (FlTouchEvent event, PieTouchResponse? response) { if (event is FlTapUpEvent response?.touchedSection ! null) { final index response!.touchedSection!.touchedSectionIndex; final item _items[index]; setState(() { _touchedIndex index; _centerLabel item.label; _centerValue item.value; }); } }, ), sections: List.generate(_items.length, (i) { final item _items[i]; final isTouched i _touchedIndex; return PieChartSectionData( value: item.value, color: item.color, radius: isTouched ? 78 : 68, title: ${item.value}%, titlePositionPercentageOffset: 0.6, titleStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white, ), showTitle: true, ); }), ), ), ), Column( mainAxisSize: MainAxisSize.min, children: [ Text( _centerLabel, style: const TextStyle(fontSize: 16, color: Colors.grey), ), Text( ${_centerValue.toStringAsFixed(0)}%, style: const TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: Colors.black87, ), ), ], ), ], ), ); } }点击切换后被选中的扇区radius从 68 变成 78会产生向外凸出的效果这个反馈非常直观。中心文字同时变成对应渠道的名称和占比。由于 fl_chart 的扇区状态变化默认有动画过渡整体体验很顺滑。动画时长用swapAnimationDuration设置曲线用swapAnimationCurve来控制不需要自己写补间动画。这里有一个容易忽略的细节FlTapUpEvent事件类型非常重要如果只判断event is FlTapDownEvent你会发现用户触摸的瞬间就触发了更新而 fl_chart 内部可能还在进行动画最终导致点击命中位置偏移。我在适配过程中就是因为这个问题点击右侧扇区时经常触发左侧扇区加上设备像素密度差异浪费了不少时间。5. 适配 OpenHarmony 时的实战心得与踩坑记录5.1 问题排查速查表把这次适配过程中遇到过的典型问题整理成表方便你以后遇到类似情况直接对照排查。现象可能原因处理方式页面白屏日志反复出现字体测量异常中文字体回退表不一致TextPainter 返回异常宽度在全局 theme 中配置 fontFamilyFallback显式指定中文字体构建报错找不到渲染后端相关符号Flutter 引擎版本与 OpenHarmony 适配引擎不一致统一 Flutter/Dart 版本优先选择官方推荐的适配引擎版本饼图显示色斑或颜色发灰渐变、阴影走了不兼容的 GPU 渲染路径关闭阴影纯色扇区加边框分隔或确认目标设备渲染稳定后再开渐变点击扇区经常命中错误区域事件桥接或屏幕密度导致命中坐标偏移结合 devicePixelRatio 校准点击位置并优先用 FlTapUpEvent 判断完成状态离线环境下运行时闪退第三方字体远程加载失败改用本地字体资源不引入 GoogleFonts 等远程字体方案扇区标题文字与中心文字重叠titlePositionPercentageOffset 设置不合理确保标题偏移量大于中心半径占比并在不同尺寸屏幕上验证动画卡顿明显debug 模式下软件渲染性能不足用 release 模式验证动画必要时减少扇区阴影和渐变5.2 三个值得记住的适配原则第一个原则是版本要锁死。OpenHarmony 上的 Flutter 引擎版本经常落后于官方主分支而 fl_chart 新版本可能会用到更新版本的 dart:ui API。如果你发现最新版 fl_chart 编译不过不要死磕检查一下当前引擎支持什么版本然后锁定一个兼容版本。稳定运行比用最新功能重要得多。第二个原则是使用最小数据集隔离问题。饼图出问题时先把数据减到三段颜色用纯色样式全部默认确认基础渲染没问题再加复杂度。这个习惯帮我在排查过程中节省了大量时间。很多开发者一遇到白屏就怀疑库本身最后排查半天发现是自己数据里有空值或者字体配置有问题。第三个原则是模拟器不能替代真机验证。我在模拟器上一切正常真机上渐变和阴影却出现色斑这个案例充分说明了差异可能存在于渲染后端和驱动层面。所以建议在开发计划里预留真机验证时间尤其是涉及图形绘制和触摸交互的功能必须以真机为准。5.3 后续可以怎么扩展这套适配验证方法并不只适用于饼图。fl_chart 里的柱状图、折线图、散点图其实都是同样思路底层依然依赖 dart:ui 的绘制能力所以适配经验大体可以复用。如果你后面要做复杂仪表盘还可以在同一份Stack方案里把饼图和环形进度组件组合起来视觉层次会更丰富。另外由于 fl_chart 是纯 Dart 实现整个适配过程基本不需要碰原生代码打包体积也不会因为引入图表库而明显上升这一点在 OpenHarmony 应用里也是加分项。6. 最后再分享一个实用技巧写到这里整个 fl_chart 适配 OpenHarmony 并实现自定义饼图的路径已经比较完整了。最后分享一个我在这类适配任务里一直坚持的实用技巧始终保留一个“最小验证 Demo”专门用来验证引擎、渲染库和交互链路是否正常。这个 Demo 不要掺杂业务代码也不要混入过多三方依赖只包含一个页面、一组静态数据和最基本的交互。当业务代码出现问题时先切到最小验证 Demo 里复现。如果 Demo 能跑说明问题出在业务侧或者依赖冲突上如果 Demo 也跑不了说明问题在引擎或图表库的适配层。这套二分法让我在排查时几乎不用猜直接能找到问题边界。这次适配饼图时我也是一开始就把三个测试场景固定在了最小验证 Demo 里静态色块、动画切换、渐变阴影。基于这三个场景去隔离问题效率非常高。另外提醒一句官方文档里写得再兼容实际跑起来要多拿模拟器和真机对比过一遍这次在模拟器上正常、真机上渐变掉色的问题就是对比后暴露出来的。希望对你有帮助。