使用 flutter 内置组件(非 canvas)实现简易 k 线图

808 阅读3分钟

早些时候我用 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 上也不会有任何性能问题。

最后

通过这次的实践,至少说明了两个问题:

  1. flutter 的渲染性能足够高效;
  2. flutter widgets 渲染性能再高和直接用 canvas 绘制仍然是有差距的;

目前该项目开源在 github.com/qiuxiang/si… ,做了简单的封装并发布到了 pub.dev,虽然作为实验性项目,我不太可能长期维护并完善更多功能。