效果圖
效果圖拆解
- 圖中包含上下兩個部分,上部分是蠟燭圖,下部分是成交量
- 兩個圖中分為兩個部分,背景的柵格和內容部分,並且有一定的關聯性
- 蠟燭圖的成交量的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自己這層的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);
}
}
複製程式碼
繪製成交量
有個蠟燭的繪製,成交量的繪製就再交單不過了
同樣的填充資料,計算最大最小值,轉換資料,繪製資料。
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);
}
}
複製程式碼
繪製網格
蠟燭和成交量都有網格,並且是在最下面的圖層
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),
],
);
}),
));
複製程式碼
總結
現在完成了蠟燭圖的繪製和成交量的繪製,算是有個初步的樣子了。文中有些程式碼比較醜,等寫完在重構整理整理。
下一節聊聊手勢處理