Flutter小小实践——KLine 绘制篇(二)
效果图
效果图拆解
- 图中包含上下两个部分,上部分是蜡烛图,下部分是成交量
- 两个图中分为两个部分,背景的栅格和内容部分,并且有一定的关联性
- 蜡烛图的成交量的X轴是时间线,并且同一个位置的时间相同,也就是垂直方向上时间是同步的。
- Y轴代表数字范围。
- 蜡烛图和成交量有很多的相似处,因此代码结构上可以复用。
抽取绘图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 做了两件事
- 创建RenderObject
- 更新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干了什么事?
我们先想想开始的效果都拆解了什么。
没错,大概是做了这些事
- 存储了RenderObject的Size
Size size;
- 效果图中蜡烛图底部还有栅格,因此还得拥有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);
}
- Widget的上边界和下边界分别是最大值和最小值,而flutter中的坐标的原点是左上角,向右是X的正反向,向下是Y的正反向,与绘制图是坐标系是不一样的,因此需要拥有坐标系变换的能力。
/// 该render自己这层的matrix4vm.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中
/// 成交量rendervolumeRender = 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