Flutter小小实践——KLine 绘制篇(二)

效果图


效果图拆解

  1. 图中包含上下两个部分,上部分是蜡烛图,下部分是成交量
  2. 两个图中分为两个部分,背景的栅格和内容部分,而且有必定的关联性
  3. 蜡烛图的成交量的X轴是时间线,而且同一个位置的时间相同,也就是垂直方向上时间是同步的。
  4. Y轴表明数字范围。
  5. 蜡烛图和成交量有不少的类似处,所以代码结构上能够复用。

抽取绘图Widget

class XRenderWidget<T extends ChangeNotifier> extends LeafRenderObjectWidget {
  BaseRender baseRender;
  XRenderWidget({Key key, this.baseRender}) : super(key: key);
  @override
  RenderObject createRenderObject(Object context) {
    try {
      Provider.of<T>(context);
    } catch (Exception) {
      // ignore
    }
    return XRenderBox(baseRender: baseRender);
  }

  @override
  void updateRenderObject(BuildContext context, XRenderBox renderObject) {
    super.updateRenderObject(context, renderObject);
    print("$baseRender updateRenderObject");
    renderObject.updateRender();
  }
}
复制代码

XRenderWidget 作了两件事canvas

  1. 建立RenderObject
  2. 更新RenderObject

在rebuild以后,updateRenderObject会被执行,和StatefulWidget相似,重复利用RenderObject来渲染UI,提升利用率bash

抽取绘图RenderBox

class XRenderBox extends RenderBox {
  BaseRender baseRender;

  XRenderBox({this.baseRender});

  @override
  void performLayout() {
    super.performLayout();
    baseRender?.onPerformLayout(size);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    super.paint(context, offset);
    baseRender?.onPaint(context, offset);
  }

  @override
  bool get sizedByParent => true;

  @override
  bool hitTestSelf(Offset position) => true;

  void updateRender() {
    baseRender?.updateRender();
    markNeedsPaint();
  }
}
复制代码

XRenderBox 很简单,静态代理了一些核心函数网络

BaseRender干了什么事?

咱们先想一想开始的效果都拆解了什么。app

没错,大概是作了这些事ide

  1. 存储了RenderObject的Size
Size size;
复制代码
  1. 效果图中蜡烛图底部还有栅格,所以还得拥有UI层次处理的能力
List<T> _aboveChildren = [];
  List<T> _underChildren = [];
  BaseMaxMinRender parent;

  void addChild(T child, {int elevation = 0}) {
    if (elevation < 0) {
      _underChildren.add(child);
    } else {
      _aboveChildren.add(child);
    }
  }

  void delChild(T child) {
    _aboveChildren.remove(child);
    _underChildren.remove(child);
  }

复制代码
  1. Widget的上边界和下边界分别是最大值和最小值,而flutter中的坐标的原点是左上角,向右是X的正反向,向下是Y的正反向,与绘制图是坐标系是不同的,所以须要拥有坐标系变换的能力。
/// 该render本身这层的matrix4
  vm.Matrix4 _matrix4 = vm.Matrix4.identity();
  /// 图层叠加后的matrix4
  vm.Matrix4 get matrix4 => parent?.matrix4 ?? _matrix4;

  void _calcMaxMin() {
    
    MaxMin newMaxMin;
    /// 该层的最大最小值
    MaxMin cur = calcOwnMaxMin();
    /// children的最大最小值
    MaxMin children = _childrenMaxMin();
    /// 没有children,或者children无需计算最大最小则是null
    if (children == null) {
      newMaxMin = cur;
    } else {
      // 把本身的最大值和最小值与children合并成新的最大最小值
      newMaxMin = cur.merge(children);
    }
    if (newMaxMin != maxMin) {
      if (maxMin == null || maxMin.isZero()) {
        /// maxMin为初始化则直接赋值
        maxMin = newMaxMin;
      } else {
        /// 最大值也最小值发生了变化,则平滑改变最大最小,体验会流程不少
        _smoothChangeMaxMin(newMaxMin);
      }
    }
  }

  /// 经过MaxMin的值,变换成K线图的正交坐标系,变换系数存储在matrix4中,方便数据变化
  void _transformMatrix() {
    if (_maxMin != null) {
      _matrix4.setIdentity();
      /// 计算Y轴的缩放值
      var scaleY = (height - edgeInsets.bottom - edgeInsets.top) / _maxMin.delta;
      /// 设置矩阵的在X轴的偏移量,由于图中的最小值并不都是0开始,所以须要在X轴上移动相应的距离
      _matrix4.setTranslation(vm.Vector3(0, height - edgeInsets.bottom + _maxMin.min * scaleY, 0.0));
      /// 设置矩阵的对角线值 对角线的值分别是x,y,z的缩放值。1表示不缩放,-scaleY表示Y轴的值都要与-scaleY相乘,所以至关因而缩放了scaleY,而且反转的Y轴的反向。
      _matrix4.setDiagonal(vm.Vector4(1, -scaleY, 1, 1));
    } else {
      _matrix4.setIdentity();
    }
  }
复制代码

绘制蜡烛图

render有了层次处理,坐标变换的能力以后,就能够方便的绘制图像了。
函数

class CandleRender extends BaseKLineRender {
  Paint _klinePaint = Paint();
  /// 屏幕显示区域的蜡烛芯数据
  List<double> wickData = [];
  /// 屏幕显示区域的蜡烛数据
  List<double> candleData = [];

  CandleRender(ControllerModel controller) : super(controller);

  Color _itemColor(int i) => controller.getColorRelativeStartIndex(i);

  @override
  void fillOwnData() {
    super.fillOwnData();
    wickData.clear();
    candleData.clear();
    /// 遍历须要显示在屏幕部分的数据
    forEachData((i) {
      double x = controller.getXByIndex(i);
      /// 三个数据表示一个点(x,y,z),这里的z是0
      /// 添加蜡烛芯的线段数据
      wickData..add(x)..add(klineData[i].high)..add(0)..add(x)..add(klineData[i].low)..add(0);
      /// 添加蜡烛体的线段数据
      candleData..add(x)..add(klineData[i].open)..add(0)..add(x)..add(klineData[i].close)..add(0);
    });
  }

  @override
  void transformData() {
    /// 把上面添加的数据,通过坐标变换,转成屏幕上的数据。
    /// 上面在添加的源数据(x,y,z),其中x已是屏幕上的像素值了,可是y是价格,y也要作必定的缩放和平移
    matrix4.applyToVector3Array(wickData);
    matrix4.applyToVector3Array(candleData);
  }

    /// 计算自身的这一层的最大最小值。
  @override
  MaxMin calcOwnMaxMin() {
    double max = -double.maxFinite;
    double min = double.maxFinite;
    for (int i = controller.startIndex; i <= controller.endIndex; i++) {
      if (i == controller.startIndex) {
        min = klineData[i].low;
        max = klineData[i].high;
      } else {
        max = math.max(max, klineData[i].high);
        min = math.min(min, klineData[i].low);
      }
    }
    return MaxMin(min: min, max: max);
  }

  @override
  void onRealPaint(Canvas canvas) {
      super.onRealPaint(canvas);
    _klinePaint.strokeWidth = 1;
    /// 蜡烛芯的画笔大小是1就能够了
    drawLines(canvas, wickData, controller.needDrawCount(), _klinePaint, color: _itemColor);
    _klinePaint.strokeWidth = controller.candleWidth - 1;
    /// 蜡烛体的画笔大小是, 蜡烛所占据的宽度 - 1, 这样蜡烛直接就有个1的空隙。比较美观点 
    drawLines(canvas, candleData, controller.needDrawCount(), _klinePaint, color: _itemColor);
  }
}
复制代码

绘制成交量

有个蜡烛的绘制,成交量的绘制就再交单不过了
一样的填充数据,计算最大最小值,转换数据,绘制数据。ui

class VolumeRender extends BaseKLineRender {
  Paint _klinePaint = Paint();
  List<double> _volData = [];

  VolumeRender(ControllerModel controller) : super(controller);

  Color _itemColor(int i) => controller.getColorRelativeStartIndex(i);

  @override
  void fillOwnData() {
    super.fillOwnData();
    _volData.clear();
    forEachData((i) {
      double x = controller.getXByIndex(i);
      _volData..add(x)..add(klineData[i].amount)..add(0)..add(x)..add(0)..add(0);
    });
  }

  @override
  void transformData() {
    matrix4.applyToVector3Array(_volData);
  }

  @override
  MaxMin calcOwnMaxMin() {
    double min = 0;
    double max = 0;
    forEachData((i) {
      max = math.max(max, klineData[i].amount);
    });
    return MaxMin(min: min, max: max);
  }

  

  @override
  void onRealPaint(Canvas canvas) {
    super.onRealPaint(canvas);
    _klinePaint.strokeWidth = controller.candleWidth - 1;
    drawLines(canvas, _volData, controller.needDrawCount(), _klinePaint, color: _itemColor);
  }
}

复制代码

绘制网格

蜡烛和成交量都有网格,而且是在最下面的图层this

class GridLineRender extends _BaseGridLineRender {
  Paint _paint = Paint();

  GridLineConfig gridLineConfig;

  GridLineRender(this.gridLineConfig, controller) : super(controller);

  @override
  void onRealPaint(Canvas canvas) {
    super.onRealPaint(canvas);
    /// 坐标变换的逆变换,用途是更加屏幕上的坐标算出对应的价格。
    /// 这样网络线上对应的价格就很方便的得知了。
    Matrix4 m = matrix4.clone()..invert();
    /// 根据配置绘制水平线
    for (int i = 0; i < gridLineConfig.horizontalCount; i++) {
      double y = height / (gridLineConfig.horizontalCount - 1) * i;
      _paint.strokeWidth = gridLineConfig.horizontalStrokeWidth;
      _paint.color = gridLineConfig.horizontalColor;
      canvas.drawLine(Offset(0, y), Offset(width, y), _paint);
      if (isNotEmpty(klineData) && totalMaxMin != null) {
        List<double> yy = [0, y, 0];
        m.applyToVector3Array(yy);
        if (i == 0) {
          drawText(canvas, "${format != null ? format(yy[1]) : yy[1]}", Offset(width, y), align: TextAlign.end);
        } else if (i == gridLineConfig.horizontalCount - 1) {
          drawText(canvas, "${format != null ? format(yy[1]) : yy[1]}", Offset(width, y - 12), align: TextAlign.end);
        } else {
          drawText(canvas, "${format != null ? format(yy[1]) : yy[1]}", Offset(width, y - 12), align: TextAlign.end);
        }
      }
    }
    /// 根据配置绘制垂直线
    for (int i = 0; i < gridLineConfig.verticalCount; i++) {
      double x = width / (gridLineConfig.verticalCount - 1) * i;
      _paint.strokeWidth = gridLineConfig.verticalStrokeWidth;
      _paint.color = gridLineConfig.verticalColor;
      canvas.drawLine(Offset(x, 0), Offset(x, height), _paint);
    }
  }
}

class GridLineConfig {
  int verticalCount = 6;
  Color verticalColor = Colors.grey[300];
  double verticalStrokeWidth = 0.5;

  int horizontalCount = 3;
  Color horizontalColor = Colors.grey[300];
  double horizontalStrokeWidth = 0.5;
}

复制代码

Render加到Widget中

/// 成交量render
volumeRender = VolumeRender(_controllerModel);
/// 蜡烛图render
candleRender = CandleRender(_controllerModel);
/// 蜡烛图添加个子render绘制底层网格
candleRender.addChild(
    GridLineRender(GridLineConfig()
        ..horizontalCount = 5, _controllerModel)
        ..format = (double val) => formatNumber(val, 2),
    elevation: -1);
/// 成交量添加子render,绘制底层网格
volumeRender.addChild(
    GridLineRender(GridLineConfig(), _controllerModel)
        ..format = (double val) => formatNumber(val, 2),
    elevation: -1);


return MultiProvider(
        providers: [
          ChangeNotifierProvider<DataModel>(create: (_) => _dataModel),
          ChangeNotifierProvider<ConfigModel>(create: (_) => _configModel),
          ChangeNotifierProvider<ControllerModel>(create: (_) => _controllerModel),
          ChangeNotifierProvider<KLineHighlightModel>(create: (_) => _hightLightModel),
        ],
        child: _wrapperGesture(
          Consumer<ControllerModel>(builder: (context, controllerModel, child) {
            _logger.debug("Consumer KLineControllerModel");
            return Column(
                  children: <Widget>[
                    xRenderWidget<DataModel>(candleRender, height: 200),
                    xRenderWidget<DataModel>(volumeRender, height: 100),
                  ],
                );
          }),
        ));
复制代码

总结

如今完成了蜡烛图的绘制和成交量的绘制,算是有个初步的样子了。文中有些代码比较丑,等写完在重构整理整理。spa

下一节聊聊手势处理debug

相关文章
相关标签/搜索