早些时候我用 canvas 实现过一个完整的 k 线图组件:github.com/qiuxiang/fl… ,如今时隔一年多,flutter 发生了一些变化,我对 flutter 开发的最佳实践也发生了一些变化。趁着最近有些时间,计划重写一个 k 线图组件,并记录总结一些当前的最佳实践。
关于 k 线图滑动实现的思考以及一个想法的产生
在我最初的实现里,k 线图的滑动是用 GestureDetector.onScaleUpdate 获取手指滑动时的 dx 实时更新图表;在滑动结束时根据 onScaleEnd 的瞬时速度,用匀减速公式计算出滑动距离和时间,再调用 AnimationController 实现滑动阻尼效果。这相当于实现了一个 ScrollView,事实上,k 线图就就是一个 scrollable widget,那我们完全可以直接用 ScrollView 实现滑动效果。
做法便是,渲染一个长度为 itemWidth * data.length 但不做任何显示的 ScrollView,通过监听 ScrollView 的 offset,在底层渲染 k 线图,即可实现和 ScrollView 一致的滑动体验。
Stack(children: [
CandlestickChart(),
CustomScrollView(
scrollDirection: Axis.horizontal,
reverse: true,
slivers: [
SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(context, index) => const SizedBox(),
childCount: state.data.length,
),
itemExtent: state.itemWidth.value,
),
],
),
])
写到这里的时候,我不禁想,在不考虑绘制折线图的情况下,是不是可以用 flutter 内置的 widgets 就渲染出 k 线图呢?这显然是可以的。k 线就只是简单的方块和直线组成而已,理论上 web、react native 也可以这么做,但我们不会这么做,因为对 web 来说,dom 的频繁操作会导致严重的性能问题,react native 也不会好到哪里去。但 flutter 呢?情况就不太一样了,因为 flutter widgets 是自己渲染的,也没有 bridge 的消耗,在我的实践经验里,flutter 的渲染性能是可以完全放心的,那么理论上主要的消耗就只是 build 函数。于是我就非常好奇,在把 build 函数优化到极致的情况下,仅靠 flutter widgets 渲染出来的 k 线图是否能做到流畅的体验?
实现
单个 k 线绘制
Widget build(BuildContext context) {
final ratio = (high - low) / widgetHeight;
final changes = item.close - item.open;
final color = changes > 0 ? Colors.green : Colors.red;
return Stack(alignment: Alignment.bottomCenter, children: [
// high-low 线
Container(
margin: EdgeInsets.only(
bottom: (item.low - state.low.value) / ratio,
),
width: 1,
height: (item.high - item.low) / ratio,
color: color,
),
// open-close 方块
Container(
margin: EdgeInsets.only(bottom: (min(item.open, item.close) - low) / ratio),
height: changes.abs() / ratio,
color: color,
),
]);
}
不得不说,用 widget 渲染 k 线比 canvas 绘制简单得太多了,需要计算的数据非常少,横向 offset 根本不需要考虑,high-low 线和 open-close 方块就只用两个 Container 就可以渲染出来。要支持动画过渡,更是只需要把 Container 换成 AnimatedContainer。
最终效果
user-images.githubusercontent.com/1709072/146…
可以看到在 app 上,滑动非常流畅,动画过渡也没有任何掉帧。
我还编译了 web 版,可以直接在浏览器里体验:qiuxiang.github.io/simple_cand…
作为对比,这是 canvas 实现的版本:qiuxiang.github.io/flutter-fin…
在移动端浏览器上由于 js 性能缺陷,build 对性能的影响直接反映到了帧率上,这时候就已经不能保证 60 帧流畅渲染了,而 canvas 的实现在 web 上也不会有任何性能问题。
最后
通过这次的实践,至少说明了两个问题:
- flutter 的渲染性能足够高效;
- flutter widgets 渲染性能再高和直接用 canvas 绘制仍然是有差距的;
目前该项目开源在 github.com/qiuxiang/si… ,做了简单的封装并发布到了 pub.dev,虽然作为实验性项目,我不太可能长期维护并完善更多功能。