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 做了两件事

  1. 创建RenderObject
  2. 更新RenderObject

在rebuild之后,updateRenderObject会被执行,和StatefulWidget类似,重复利用RenderObject来渲染UI,提高利用率

抽取绘图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干了什么事?

我们先想想开始的效果都拆解了什么。

没错,大概是做了这些事

  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

MaxMincalcOwnMaxMin() {

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);

}

}

绘制成交量

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

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

MaxMincalcOwnMaxMin() {

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);

}

}

绘制网格

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

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);

} elseif (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),

],

);

}),

));

总结

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

下一节聊聊手势处理


以上是 Flutter小小实践——KLine 绘制篇(二) 的全部内容, 来源链接: utcz.com/a/33260.html

回到顶部