| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 |
- import 'dart:async' show StreamSink;
- import 'package:flutter/material.dart';
- import 'package:k_chart_plus/utils/number_util.dart';
- import '../entity/info_window_entity.dart';
- import '../entity/k_line_entity.dart';
- import '../utils/date_format_util.dart';
- import 'base_chart_painter.dart';
- import 'base_chart_renderer.dart';
- import 'base_dimension.dart';
- import 'main_renderer.dart';
- import 'secondary_renderer.dart';
- import 'vol_renderer.dart';
- class TrendLine {
- final Offset p1;
- final Offset p2;
- final double maxHeight;
- final double scale;
- TrendLine(this.p1, this.p2, this.maxHeight, this.scale);
- }
- double? trendLineX;
- double getTrendLineX() {
- return trendLineX ?? 0;
- }
- class ChartPainter extends BaseChartPainter {
- final List<TrendLine> lines; //For TrendLine
- final bool isTrendLine; //For TrendLine
- bool isrecordingCord = false; //For TrendLine
- final double selectY; //For TrendLine
- static get maxScrollX => BaseChartPainter.maxScrollX;
- late BaseChartRenderer mMainRenderer;
- BaseChartRenderer? mVolRenderer;
- Set<BaseChartRenderer> mSecondaryRendererList = {};
- StreamSink<InfoWindowEntity?> sink;
- Color? upColor, dnColor;
- Color? ma5Color, ma10Color, ma30Color;
- Color? volColor;
- Color? macdColor, difColor, deaColor, jColor;
- int fixedLength;
- List<int> maDayList;
- final ChartColors chartColors;
- late Paint selectPointPaint, selectorBorderPaint, nowPricePaint;
- final ChartStyle chartStyle;
- final bool hideGrid;
- final bool showNowPrice;
- final VerticalTextAlignment verticalTextAlignment;
- final BaseDimension baseDimension;
- /// 外部传入的当前价格字符串(来自 WS ticker),优先于 toStringAsFixed 显示
- final String? nowPriceStr;
- ChartPainter(
- this.chartStyle,
- this.chartColors, {
- required this.lines, //For TrendLine
- required this.isTrendLine, //For TrendLine
- required this.selectY, //For TrendLine
- required this.sink,
- required datas,
- required scaleX,
- required scrollX,
- required isLongPass,
- required selectX,
- required xFrontPadding,
- required this.baseDimension,
- isOnTap,
- isTapShowInfoDialog,
- required this.verticalTextAlignment,
- mainStateLi,
- volHidden,
- secondaryStateLi,
- bool isLine = false,
- this.hideGrid = false,
- this.showNowPrice = true,
- this.fixedLength = 2,
- this.maDayList = const [5, 10, 20],
- this.nowPriceStr,
- }) : super(chartStyle,
- datas: datas,
- scaleX: scaleX,
- scrollX: scrollX,
- isLongPress: isLongPass,
- baseDimension: baseDimension,
- isOnTap: isOnTap,
- isTapShowInfoDialog: isTapShowInfoDialog,
- selectX: selectX,
- mainStateLi: mainStateLi,
- volHidden: volHidden,
- secondaryStateLi: secondaryStateLi,
- xFrontPadding: xFrontPadding,
- isLine: isLine) {
- selectPointPaint = Paint()
- ..isAntiAlias = true
- ..strokeWidth = 0.5
- ..color = this.chartColors.selectFillColor;
- selectorBorderPaint = Paint()
- ..isAntiAlias = true
- ..strokeWidth = 0.5
- ..style = PaintingStyle.stroke
- ..color = this.chartColors.selectBorderColor;
- nowPricePaint = Paint()
- ..strokeWidth = this.chartStyle.nowPriceLineWidth
- ..isAntiAlias = true;
- }
- @override
- void initChartRenderer() {
- if (datas != null && datas!.isNotEmpty) {
- var t = datas![0];
- fixedLength =
- NumberUtil.getMaxDecimalLength(t.open, t.close, t.high, t.low);
- }
- mMainRenderer = MainRenderer(
- mMainRect,
- mMainMaxValue,
- mMainMinValue,
- mTopPadding,
- mainStateLi.toList(),
- isLine,
- fixedLength,
- this.chartStyle,
- this.chartColors,
- this.scaleX,
- verticalTextAlignment,
- maDayList,
- );
- if (mVolRect != null) {
- mVolRenderer = VolRenderer(mVolRect!, mVolMaxValue, mVolMinValue,
- mChildPadding, fixedLength, this.chartStyle, this.chartColors);
- }
- mSecondaryRendererList.clear();
- for (int i = 0; i < mSecondaryRectList.length; ++i) {
- mSecondaryRendererList.add(SecondaryRenderer(
- mSecondaryRectList[i].mRect,
- mSecondaryRectList[i].mMaxValue,
- mSecondaryRectList[i].mMinValue,
- mChildPadding,
- secondaryStateLi.elementAt(i),
- fixedLength,
- chartStyle,
- chartColors,
- ));
- }
- }
- @override
- void drawBg(Canvas canvas, Size size) {
- Paint mBgPaint = Paint()..color = chartColors.bgColor;
- Rect mainRect =
- Rect.fromLTRB(0, 0, mMainRect.width, mMainRect.height + mTopPadding);
- canvas.drawRect(mainRect, mBgPaint);
- if (mVolRect != null) {
- Rect volRect = Rect.fromLTRB(
- 0, mVolRect!.top - mChildPadding, mVolRect!.width, mVolRect!.bottom);
- canvas.drawRect(volRect, mBgPaint);
- }
- for (int i = 0; i < mSecondaryRectList.length; ++i) {
- Rect? mSecondaryRect = mSecondaryRectList[i].mRect;
- Rect secondaryRect = Rect.fromLTRB(0, mSecondaryRect.top - mChildPadding,
- mSecondaryRect.width, mSecondaryRect.bottom);
- canvas.drawRect(secondaryRect, mBgPaint);
- }
- Rect dateRect =
- Rect.fromLTRB(0, size.height - mBottomPadding, size.width, size.height);
- canvas.drawRect(dateRect, mBgPaint);
- }
- @override
- void drawGrid(canvas) {
- if (!hideGrid) {
- mMainRenderer.drawGrid(canvas, mGridRows, mGridColumns);
- mVolRenderer?.drawGrid(canvas, mGridRows, mGridColumns);
- mSecondaryRendererList.forEach((element) {
- element.drawGrid(canvas, mGridRows, mGridColumns);
- });
- }
- }
- @override
- void drawChart(Canvas canvas, Size size) {
- canvas.save();
- canvas.translate(mTranslateX * scaleX, 0.0);
- canvas.scale(scaleX, 1.0);
- for (int i = mStartIndex; datas != null && i <= mStopIndex; i++) {
- KLineEntity? curPoint = datas?[i];
- if (curPoint == null) continue;
- KLineEntity lastPoint = i == 0 ? curPoint : datas![i - 1];
- double curX = getX(i);
- double lastX = i == 0 ? curX : getX(i - 1);
- mMainRenderer.drawChart(lastPoint, curPoint, lastX, curX, size, canvas);
- mVolRenderer?.drawChart(lastPoint, curPoint, lastX, curX, size, canvas);
- mSecondaryRendererList.forEach((element) {
- element.drawChart(lastPoint, curPoint, lastX, curX, size, canvas);
- });
- }
- if ((isLongPress == true || (isTapShowInfoDialog && isOnTap)) &&
- isTrendLine == false) {
- drawCrossLine(canvas, size);
- }
- if (isTrendLine == true) drawTrendLines(canvas, size);
- canvas.restore();
- }
- @override
- void drawVerticalText(canvas) {
- var textStyle = getTextStyle(this.chartColors.defaultTextColor);
- if (!hideGrid) {
- mMainRenderer.drawVerticalText(canvas, textStyle, mGridRows);
- }
- mVolRenderer?.drawVerticalText(canvas, textStyle, mGridRows);
- mSecondaryRendererList.forEach((element) {
- element.drawVerticalText(canvas, textStyle, mGridRows);
- });
- }
- @override
- void drawDate(Canvas canvas, Size size) {
- if (datas == null) return;
- double columnSpace = size.width / mGridColumns;
- double startX = getX(mStartIndex) - mPointWidth / 2;
- double stopX = getX(mStopIndex) + mPointWidth / 2;
- double x = 0.0;
- double y = 0.0;
- for (var i = 0; i <= mGridColumns; ++i) {
- double translateX = xToTranslateX(columnSpace * i);
- if (translateX >= startX && translateX <= stopX) {
- int index = indexOfTranslateX(translateX);
- if (datas?[index] == null) continue;
- TextPainter tp = getTextPainter(getDate(datas![index].time), null);
- // 时间轴绘制在图表底部 bottomPadding 区域,避免与 VOL 标签重叠
- y = size.height - mBottomPadding + (mBottomPadding - tp.height) / 2;
- x = columnSpace * i - tp.width / 2;
- // Prevent date text out of canvas
- if (x < 0) x = 0;
- if (x > size.width - tp.width) x = size.width - tp.width;
- tp.paint(canvas, Offset(x, y));
- }
- }
- // double translateX = xToTranslateX(0);
- // if (translateX >= startX && translateX <= stopX) {
- // TextPainter tp = getTextPainter(getDate(datas[mStartIndex].id));
- // tp.paint(canvas, Offset(0, y));
- // }
- // translateX = xToTranslateX(size.width);
- // if (translateX >= startX && translateX <= stopX) {
- // TextPainter tp = getTextPainter(getDate(datas[mStopIndex].id));
- // tp.paint(canvas, Offset(size.width - tp.width, y));
- // }
- }
- /// draw the cross line. when user focus
- @override
- void drawCrossLineText(Canvas canvas, Size size) {
- var index = calculateSelectedX(selectX);
- KLineEntity point = getItem(index);
- TextPainter tp = getTextPainter(point.close, chartColors.crossTextColor);
- double textHeight = tp.height;
- double textWidth = tp.width;
- double w1 = 5;
- double w2 = 3;
- double r = textHeight / 2 + w2;
- double y = getMainY(point.close);
- double x;
- bool isLeft = false;
- if (translateXtoX(getX(index)) < mWidth / 2) {
- isLeft = false;
- x = 1;
- Path path = new Path();
- path.moveTo(x, y - r);
- path.lineTo(x, y + r);
- path.lineTo(textWidth + 2 * w1, y + r);
- path.lineTo(textWidth + 2 * w1 + w2, y);
- path.lineTo(textWidth + 2 * w1, y - r);
- path.close();
- canvas.drawPath(path, selectPointPaint);
- canvas.drawPath(path, selectorBorderPaint);
- tp.paint(canvas, Offset(x + w1, y - textHeight / 2));
- } else {
- isLeft = true;
- x = mWidth - textWidth - 1 - 2 * w1 - w2;
- Path path = new Path();
- path.moveTo(x, y);
- path.lineTo(x + w2, y + r);
- path.lineTo(mWidth - 2, y + r);
- path.lineTo(mWidth - 2, y - r);
- path.lineTo(x + w2, y - r);
- path.close();
- canvas.drawPath(path, selectPointPaint);
- canvas.drawPath(path, selectorBorderPaint);
- tp.paint(canvas, Offset(x + w1 + w2, y - textHeight / 2));
- }
- TextPainter dateTp =
- getTextPainter(getDate(point.time), chartColors.crossTextColor);
- textWidth = dateTp.width;
- r = textHeight / 2;
- x = translateXtoX(getX(index));
- y = size.height - mBottomPadding;
- if (x < textWidth + 2 * w1) {
- x = 1 + textWidth / 2 + w1;
- } else if (mWidth - x < textWidth + 2 * w1) {
- x = mWidth - 1 - textWidth / 2 - w1;
- }
- double baseLine = textHeight / 2;
- canvas.drawRect(
- Rect.fromLTRB(x - textWidth / 2 - w1, y, x + textWidth / 2 + w1,
- y + baseLine + r),
- selectPointPaint);
- canvas.drawRect(
- Rect.fromLTRB(x - textWidth / 2 - w1, y, x + textWidth / 2 + w1,
- y + baseLine + r),
- selectorBorderPaint);
- dateTp.paint(canvas, Offset(x - textWidth / 2, y));
- //Long press to display the details of this data
- sink.add(InfoWindowEntity(point, isLeft: isLeft));
- }
- @override
- void drawText(Canvas canvas, KLineEntity data, double x) {
- //Long press to display the data in the press
- if (isLongPress || (isTapShowInfoDialog && isOnTap)) {
- var index = calculateSelectedX(selectX);
- data = getItem(index);
- }
- //Release to display the last data
- mMainRenderer.drawText(canvas, data, x);
- mVolRenderer?.drawText(canvas, data, x);
- mSecondaryRendererList.forEach((element) {
- element.drawText(canvas, data, x);
- });
- }
- @override
- void drawMaxAndMin(Canvas canvas) {
- if (isLine == true) return;
- //plot maxima and minima
- double x = translateXtoX(getX(mMainMinIndex));
- double y = getMainY(mMainLowMinValue);
- if (x < mWidth / 2) {
- //draw right
- TextPainter tp = getTextPainter(
- "── " + mMainLowMinValue.toStringAsFixed(fixedLength),
- chartColors.minColor);
- tp.paint(canvas, Offset(x, y - tp.height / 2));
- } else {
- TextPainter tp = getTextPainter(
- mMainLowMinValue.toStringAsFixed(fixedLength) + " ──",
- chartColors.minColor);
- tp.paint(canvas, Offset(x - tp.width, y - tp.height / 2));
- }
- x = translateXtoX(getX(mMainMaxIndex));
- y = getMainY(mMainHighMaxValue);
- if (x < mWidth / 2) {
- //draw right
- TextPainter tp = getTextPainter(
- "── " + mMainHighMaxValue.toStringAsFixed(fixedLength),
- chartColors.maxColor);
- tp.paint(canvas, Offset(x, y - tp.height / 2));
- } else {
- TextPainter tp = getTextPainter(
- mMainHighMaxValue.toStringAsFixed(fixedLength) + " ──",
- chartColors.maxColor);
- tp.paint(canvas, Offset(x - tp.width, y - tp.height / 2));
- }
- }
- @override
- void drawNowPrice(Canvas canvas) {
- if (!this.showNowPrice) {
- return;
- }
- if (datas == null) {
- return;
- }
- double value = datas!.last.close;
- double y = getMainY(value);
- //view display area boundary value drawing
- if (y > getMainY(mMainLowMinValue)) {
- y = getMainY(mMainLowMinValue);
- }
- if (y < getMainY(mMainHighMaxValue)) {
- y = getMainY(mMainHighMaxValue);
- }
- nowPricePaint
- ..color = value >= datas!.last.open
- ? this.chartColors.nowPriceUpColor
- : this.chartColors.nowPriceDnColor;
- //first draw the horizontal line
- double startX = 0;
- final max = -mTranslateX + mWidth / scaleX;
- final space =
- this.chartStyle.nowPriceLineSpan + this.chartStyle.nowPriceLineLength;
- while (startX < max) {
- canvas.drawLine(
- Offset(startX, y),
- Offset(startX + this.chartStyle.nowPriceLineLength, y),
- nowPricePaint);
- startX += space;
- }
- //repaint the background and text
- TextPainter tp = getTextPainter(
- nowPriceStr ?? value.toStringAsFixed(fixedLength),
- this.chartColors.nowPriceTextColor,
- );
- double offsetX;
- switch (verticalTextAlignment) {
- case VerticalTextAlignment.left:
- offsetX = 0;
- break;
- case VerticalTextAlignment.right:
- offsetX = mWidth - tp.width;
- break;
- }
- double top = y - tp.height / 2;
- canvas.drawRect(
- Rect.fromLTRB(offsetX, top, offsetX + tp.width, top + tp.height),
- nowPricePaint);
- tp.paint(canvas, Offset(offsetX, top));
- }
- //For TrendLine
- void drawTrendLines(Canvas canvas, Size size) {
- var index = calculateSelectedX(selectX);
- Paint paintY = Paint()
- ..color = chartColors.trendLineColor
- ..strokeWidth = 1
- ..isAntiAlias = true;
- double x = getX(index);
- trendLineX = x;
- double y = selectY;
- // getMainY(point.close);
- // K-line chart vertical line
- canvas.drawLine(Offset(x, mTopPadding),
- Offset(x, size.height - mBottomPadding), paintY);
- Paint paintX = Paint()
- ..color = chartColors.trendLineColor
- ..strokeWidth = 1
- ..isAntiAlias = true;
- Paint paint = Paint()
- ..color = chartColors.trendLineColor
- ..strokeWidth = 1.0
- ..style = PaintingStyle.stroke
- ..strokeCap = StrokeCap.round;
- canvas.drawLine(Offset(-mTranslateX, y),
- Offset(-mTranslateX + mWidth / scaleX, y), paintX);
- if (scaleX >= 1) {
- canvas.drawOval(
- Rect.fromCenter(
- center: Offset(x, y), height: 15.0 * scaleX, width: 15.0),
- paint,
- );
- } else {
- canvas.drawOval(
- Rect.fromCenter(
- center: Offset(x, y), height: 10.0, width: 10.0 / scaleX),
- paint,
- );
- }
- if (lines.isNotEmpty) {
- lines.forEach((element) {
- var y1 = -((element.p1.dy - 35) / element.scale) + element.maxHeight;
- var y2 = -((element.p2.dy - 35) / element.scale) + element.maxHeight;
- var a = (trendLineMax! - y1) * trendLineScale! + trendLineContentRec!;
- var b = (trendLineMax! - y2) * trendLineScale! + trendLineContentRec!;
- var p1 = Offset(element.p1.dx, a);
- var p2 = Offset(element.p2.dx, b);
- canvas.drawLine(
- p1,
- element.p2 == Offset(-1, -1) ? Offset(x, y) : p2,
- Paint()
- ..color = Colors.yellow
- ..strokeWidth = 2);
- });
- }
- }
- ///draw cross lines
- void drawCrossLine(Canvas canvas, Size size) {
- var index = calculateSelectedX(selectX);
- KLineEntity point = getItem(index);
- Paint paintY = Paint()
- ..color = this.chartColors.vCrossColor
- ..strokeWidth = this.chartStyle.vCrossWidth
- ..isAntiAlias = true;
- double x = getX(index);
- double y = getMainY(point.close);
- // K-line chart vertical line
- canvas.drawLine(Offset(x, mTopPadding),
- Offset(x, size.height - mBottomPadding), paintY);
- Paint paintX = Paint()
- ..color = this.chartColors.hCrossColor
- ..strokeWidth = this.chartStyle.hCrossWidth
- ..isAntiAlias = true;
- // K-line chart horizontal line
- canvas.drawLine(Offset(-mTranslateX, y),
- Offset(-mTranslateX + mWidth / scaleX, y), paintX);
- if (scaleX >= 1) {
- canvas.drawOval(
- Rect.fromCenter(center: Offset(x, y), height: 2.0 * scaleX, width: 2.0),
- paintX,
- );
- } else {
- canvas.drawOval(
- Rect.fromCenter(center: Offset(x, y), height: 2.0, width: 2.0 / scaleX),
- paintX,
- );
- }
- }
- TextPainter getTextPainter(text, color) {
- if (color == null) {
- color = this.chartColors.defaultTextColor;
- }
- TextSpan span = TextSpan(text: "$text", style: getTextStyle(color));
- TextPainter tp = TextPainter(text: span, textDirection: TextDirection.ltr);
- tp.layout();
- return tp;
- }
- String getDate(int? date) => dateFormat(
- DateTime.fromMillisecondsSinceEpoch(
- date ?? DateTime.now().millisecondsSinceEpoch),
- mFormats,
- );
- double getMainY(double y) => mMainRenderer.getY(y);
- /// Whether the point is in the SecondaryRect
- // bool isInSecondaryRect(Offset point) {
- // // return mSecondaryRect.contains(point) == true);
- // return false;
- // }
- /// Whether the point is in MainRect
- bool isInMainRect(Offset point) {
- return mMainRect.contains(point);
- }
- }
|