Flutter绘制k线图——增加当前值水平线,最后的柱子跟随变化,长按事件

904 阅读4分钟

**首先效果上图:

再上代码

注意开头的时候,动态增加了4条数据,柱子和线都变了,如果你们用websocket或者tcp传输不间断数据,就动态了。 now,let‘s go。

分为三节:

1、完成静态单页面K线图

2、增加水平滑动和双指缩放

3、增加当前值水平线,最后的柱子跟随变化,长按事件

思想:水平线,在第一节的基础上计算一下纵坐标,画条线即可,虚线我是直接用的别人写好的api画的虚线。柱子跟随当前行情变化,即通过改变数据集的最后一条即可,这个地方注意,假如用5分图,那么当时间戳之差大于560时要增加数据,如果小于560,那么直接改变最后一个数据。这个地方具体和后端沟通即可。长按事件即增加一个事件……在左上角多绘制几个字,至于5日图,10日图,用集合存起来,在适当的时候计算即可。

preData 即和日线图有关系,因为日线图是指之前几日的平均值,前10跳数据中,只能计算出后5条数据的线,前5条数据不够,所以这个maxPreDay是指 max{几日图,几日图,几日图}。即5,10,15图的画,这里前15条数据永远不会绘制出来,值仅用于计算均值。

  child: OHLCVGraph(
              preData: _subList(start -  widget.maxPreDay, end: start),
              currentValue: widget.dataList.last['close'],
              data: _data,

currentValue 这个值就是当前值,通过setState改变。通过这个价格,动态计算出坐标。

  double currentLine = height - (currentValue - _min) * heightNormalizer;
  for (int i = 0; i < data.length; i++) {
      if (timeType == TimeType.day) {
      //分别是5日图缓存,10日图缓存,和15日图缓存。此处为了减小计算量而设置的缓存
        _minCache.add(data[i]["close"]);
        _mediumCache.add(data[i]["close"]);
        _maxCache.add(data[i]["close"]);
        //5天图

        double minValue = getAvg(_minCache, minDayLine);//算出平均值
        _minDayLineList.add(minValue);//加入集合中
        _minCache.removeAt(0);//移除第一个值,第一次循环是[0,5),第二次是[1,6),类推。10日同理

画出这个点到上一个点的线,并把当前点设置为上一个点, 最开始的上一个点为preData中算出来的

  if (lastMinOffset != null) {
            canvas.drawLine(lastMinOffset, currentOffset, minDayPaint);
          }
          lastMinOffset = currentOffset;

这样日线即完成了。

然后长按事件,长按时传入两个参数,是否长按和长按的x坐标。倘若长按了,那么通过横坐标找到中心点然后绘制

//传进来一个横坐标,通过二分法找到这个横坐标最接近坐标中心的柱子的中心坐标
 int positionX = middle2(herizontalLocalValues, 0,
            herizontalLocalValues.length - 1, longPressX, rectWidth / 2);
            
//取出herizontalLocalValues中早已存好的中心坐标
 double x = herizontalLocalValues[positionX];
  double yValue = data[positionX]['close'];//取出对应的y坐标
    double y = height - (yValue - _min) * heightNormalizer;
    //画一横一竖
    canvas.drawLine(Offset(x, 0), Offset(x, thisHeight), pressPoint);
   canvas.drawLine(Offset(0, y), Offset(width, y), pressPoint);

最后处理柱子随当前行情变化而变化,这个要自己和后端讨论了。

 int  id = glodMap['currentTime'];
        double  sellOutPrice = glodMap['sellOutPrice'];
        Map currentMap;
        if(t.tick == 0){
          currentMap = {
            'id': id,
            'open': dataList.last['close'],
            'high': dataList.last['close'] > sellOutPrice ? dataList.last['close'] : sellOutPrice,//当前值与收盘价比较
            'low': dataList.last['close'] > sellOutPrice ? sellOutPrice : dataList.last['close'],
            'close':sellOutPrice,
            'volumeto':  5000 + (Random().nextInt(5000)) +0.001
          };
        }else{
          currentMap = {
            'id': id,
            'open': dataList[dataList.length -2]['close'],
            'high': dataList[dataList.length -2]['close'] > sellOutPrice ? dataList[dataList.length -2]['close'] : sellOutPrice,
            'low': dataList[dataList.length -2]['close'] > sellOutPrice ? sellOutPrice: dataList[dataList.length -2]['close'] ,
            'close':sellOutPrice,
            'volumeto':  5000 + (Random().nextInt(5000)) +0.001
          };
        }
        //此处3600表示时间戳之差,现在表示一小时的k图
        if(id - dataList.last['id'] > 3600){ //如果和上次时间超过1小时,那么增加一个柱子
          dataList.add(currentMap);
          setState(() {

          });
        }else{//否则替换最后一个柱子
          //id不同才改变。如果用tcp或者websocket,此处不用加判断。
          if(id != dataList.last['id']){
            dataList.last = currentMap;
            setState(() {});
          }
        }

至于左上角写字,想必看到现在的人肯定可以自行解决了

 String leftShow =
            '开${data[positionX]['open'].toStringAsFixed(
            2)} 高${data[positionX]['high'].toStringAsFixed(
            2)} 低${data[positionX]['low'].toStringAsFixed(
            2)} 关${data[positionX]['close'].toStringAsFixed(2)}';
        TextPainter currentPressTextPaint = TextPainter(
            text: new TextSpan(
                text: leftShow,
                style: new TextStyle(
                    color: Colors.purple,
                    fontSize: 12.0,
                    fontWeight: FontWeight.bold)),
            textDirection: TextDirection.ltr);
        currentPressTextPaint.layout();
        currentPressTextPaint.paint(canvas, Offset(3.0, 3.0));

至于二分法

int middle2(List<double> list, int start, int end, double currentValue,
      allowScope) {
    int index = 0;
    int middle = ((start + end) / 2).toInt();
    double errorScope = (list[middle] - currentValue).abs();
    if (errorScope < allowScope) {
      return middle;
    } else if ((index++) < 10) {
      if (currentValue <= list[middle]) {
        print('middle=$middle errorScope=$errorScope allowScope=$allowScope');
        return middle2(list, start, middle, currentValue, allowScope);
      } else {
        return middle2(list, middle, end, currentValue, allowScope);
      }
    }
  }

当前值需要通过if判断是否在合理的范围内,不然可能绘制到非坐标区,毕竟size只是通知你,并不限制你。

至此,k线的静态,动态已经完成。