**首先效果上图:

注意开头的时候,动态增加了4条数据,柱子和线都变了,如果你们用websocket或者tcp传输不间断数据,就动态了。 now,let‘s go。
分为三节:
思想:水平线,在第一节的基础上计算一下纵坐标,画条线即可,虚线我是直接用的别人写好的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线的静态,动态已经完成。