fix(kline-chart): improve pinch-to-zoom and fullscreen display
- Refactor to pixel-based scrolling system for smoother interaction - Fix pinch-to-zoom to properly scale around focal point - Adjust fullscreen layout to give more space to main chart (65%) - Add candleWidth parameter to all painters for consistent rendering - Detect multi-touch gestures using pointerCount Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0ebb0ad076
commit
27bf67e561
|
|
@ -42,17 +42,18 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
static const Color _grayText = Color(0xFF6B7280);
|
||||
static const Color _borderGray = Color(0xFFE5E7EB);
|
||||
|
||||
// 缩放和滑动状态
|
||||
double _scale = 1.0;
|
||||
double _prevScale = 1.0;
|
||||
double _offsetX = 0.0;
|
||||
double _startOffsetX = 0.0;
|
||||
// 缩放和滑动状态 - 基于candleWidth的缩放模式(大厂标准做法)
|
||||
double _candleWidth = 8.0; // 单根K线宽度(像素)
|
||||
double _prevCandleWidth = 8.0;
|
||||
double _scrollX = 0.0; // 滚动偏移(像素)
|
||||
double _startScrollX = 0.0;
|
||||
Offset? _startFocalPoint;
|
||||
bool _isScaling = false;
|
||||
double _chartWidth = 0.0; // 缓存图表宽度
|
||||
|
||||
// 可见K线数量范围
|
||||
int _visibleCandleCount = 60;
|
||||
static const int _minVisibleCandles = 20;
|
||||
static const int _maxVisibleCandles = 200;
|
||||
// K线宽度范围
|
||||
static const double _minCandleWidth = 3.0;
|
||||
static const double _maxCandleWidth = 30.0;
|
||||
|
||||
// 指标选择
|
||||
int _selectedMainIndicator = 0; // 0: MA, 1: EMA, 2: BOLL
|
||||
|
|
@ -72,13 +73,13 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
}
|
||||
|
||||
void _scrollToEnd() {
|
||||
if (widget.klines.isEmpty) return;
|
||||
final totalCandles = widget.klines.length;
|
||||
if (totalCandles > _visibleCandleCount) {
|
||||
setState(() {
|
||||
_offsetX = (totalCandles - _visibleCandleCount).toDouble();
|
||||
});
|
||||
}
|
||||
if (widget.klines.isEmpty || _chartWidth == 0) return;
|
||||
// 滚动到最右边(显示最新数据)
|
||||
final totalWidth = widget.klines.length * _candleWidth;
|
||||
final maxScroll = math.max(0.0, totalWidth - _chartWidth);
|
||||
setState(() {
|
||||
_scrollX = maxScroll;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -140,8 +141,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
icon: const Icon(Icons.refresh, color: _orange),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_scale = 1.0;
|
||||
_visibleCandleCount = 60;
|
||||
_candleWidth = 8.0;
|
||||
_scrollToEnd();
|
||||
});
|
||||
},
|
||||
|
|
@ -282,6 +282,9 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
final chartHeight = height ?? constraints.maxHeight;
|
||||
final chartWidth = constraints.maxWidth;
|
||||
|
||||
// 缓存图表宽度用于滚动计算
|
||||
_chartWidth = chartWidth;
|
||||
|
||||
if (widget.klines.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
|
|
@ -295,8 +298,8 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
// 计算可见数据
|
||||
final visibleData = _getVisibleData();
|
||||
// 计算可见数据(基于像素滚动)
|
||||
final visibleData = _getVisibleData(chartWidth);
|
||||
|
||||
// 计算指标数据
|
||||
final processor = KlineDataProcessor(widget.klines);
|
||||
|
|
@ -307,15 +310,15 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
final bollData = processor.calculateBOLL();
|
||||
final emaData = processor.calculateEMA([5, 10, 20]);
|
||||
|
||||
// 计算各区域高度
|
||||
// 计算各区域高度 - 全屏模式给主图更多空间
|
||||
final mainChartHeight = widget.showVolume
|
||||
? (widget.isFullScreen ? chartHeight * 0.5 : chartHeight * 0.6)
|
||||
: chartHeight * 0.7;
|
||||
? (widget.isFullScreen ? chartHeight * 0.65 : chartHeight * 0.6)
|
||||
: (widget.isFullScreen ? chartHeight * 0.75 : chartHeight * 0.7);
|
||||
final volumeHeight = widget.showVolume
|
||||
? (widget.isFullScreen ? chartHeight * 0.15 : chartHeight * 0.2)
|
||||
? (widget.isFullScreen ? chartHeight * 0.12 : chartHeight * 0.2)
|
||||
: 0.0;
|
||||
final indicatorHeight = widget.isFullScreen
|
||||
? chartHeight * 0.25
|
||||
? (widget.showVolume ? chartHeight * 0.23 : chartHeight * 0.25)
|
||||
: (widget.showVolume ? chartHeight * 0.2 : chartHeight * 0.3);
|
||||
|
||||
return Column(
|
||||
|
|
@ -333,6 +336,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
emaData: _selectedMainIndicator == 1 ? _getVisibleMAData(emaData, visibleData.startIndex) : null,
|
||||
bollData: _selectedMainIndicator == 2 ? _getVisibleBollData(bollData, visibleData.startIndex) : null,
|
||||
crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1,
|
||||
candleWidth: visibleData.candleWidth,
|
||||
),
|
||||
),
|
||||
// 十字线信息
|
||||
|
|
@ -371,6 +375,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
painter: KlineVolumePainter(
|
||||
klines: visibleData.klines,
|
||||
crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1,
|
||||
candleWidth: visibleData.candleWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -385,6 +390,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
kdjData: _selectedSubIndicator == 1 ? _getVisibleKdjData(kdjData, visibleData.startIndex) : null,
|
||||
rsiData: _selectedSubIndicator == 2 ? _getVisibleRsiData(rsiData, visibleData.startIndex) : null,
|
||||
crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1,
|
||||
candleWidth: visibleData.candleWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -432,39 +438,49 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
// 手势处理
|
||||
// 手势处理 - 大厂标准做法:基于像素的滚动和缩放
|
||||
void _onScaleStart(ScaleStartDetails details) {
|
||||
_prevScale = _scale;
|
||||
_startOffsetX = _offsetX;
|
||||
_prevCandleWidth = _candleWidth;
|
||||
_startScrollX = _scrollX;
|
||||
_startFocalPoint = details.focalPoint;
|
||||
// 检测是否为两指缩放(pointerCount > 1表示多指触摸)
|
||||
_isScaling = details.pointerCount > 1;
|
||||
}
|
||||
|
||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
setState(() {
|
||||
// 缩放
|
||||
if (details.scale != 1.0) {
|
||||
final newScale = (_prevScale * details.scale).clamp(0.5, 3.0);
|
||||
_scale = newScale;
|
||||
// 两指缩放:调整K线宽度,围绕焦点缩放
|
||||
if (_isScaling || (details.scale - 1.0).abs() > 0.01) {
|
||||
_isScaling = true;
|
||||
final newCandleWidth = (_prevCandleWidth * details.scale).clamp(_minCandleWidth, _maxCandleWidth);
|
||||
|
||||
// 根据缩放调整可见K线数量
|
||||
_visibleCandleCount = (60 / _scale).round().clamp(_minVisibleCandles, _maxVisibleCandles);
|
||||
// 围绕焦点缩放:调整滚动位置以保持焦点处的K线位置不变
|
||||
if (_startFocalPoint != null && _chartWidth > 0) {
|
||||
final focalRatio = _startFocalPoint!.dx / _chartWidth;
|
||||
final oldFocalIndex = (_startScrollX + _startFocalPoint!.dx) / _prevCandleWidth;
|
||||
final newFocalX = oldFocalIndex * newCandleWidth;
|
||||
_scrollX = (newFocalX - focalRatio * _chartWidth).clamp(
|
||||
0.0,
|
||||
math.max(0.0, widget.klines.length * newCandleWidth - _chartWidth),
|
||||
);
|
||||
}
|
||||
|
||||
_candleWidth = newCandleWidth;
|
||||
}
|
||||
|
||||
// 平移
|
||||
if (_startFocalPoint != null) {
|
||||
// 单指平移(非缩放模式时)- 直接使用像素位移
|
||||
if (!_isScaling && _startFocalPoint != null) {
|
||||
final dx = details.focalPoint.dx - _startFocalPoint!.dx;
|
||||
// 每移动一定像素对应移动一根K线
|
||||
final candleShift = -dx / 10;
|
||||
_offsetX = (_startOffsetX + candleShift).clamp(
|
||||
0.0,
|
||||
math.max(0.0, (widget.klines.length - _visibleCandleCount).toDouble()),
|
||||
);
|
||||
final totalWidth = widget.klines.length * _candleWidth;
|
||||
final maxScroll = math.max(0.0, totalWidth - _chartWidth);
|
||||
_scrollX = (_startScrollX - dx).clamp(0.0, maxScroll);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onScaleEnd(ScaleEndDetails details) {
|
||||
_startFocalPoint = null;
|
||||
_isScaling = false;
|
||||
}
|
||||
|
||||
void _onLongPressStart(LongPressStartDetails details) {
|
||||
|
|
@ -484,40 +500,46 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
}
|
||||
|
||||
void _updateCrossLine(Offset position) {
|
||||
if (widget.klines.isEmpty) return;
|
||||
if (widget.klines.isEmpty || _chartWidth == 0) return;
|
||||
|
||||
setState(() {
|
||||
_showCrossLine = true;
|
||||
|
||||
// 计算对应的K线索引
|
||||
final visibleData = _getVisibleData();
|
||||
if (visibleData.klines.isEmpty) return;
|
||||
final candleWidth = (MediaQuery.of(context).size.width - 32) / visibleData.klines.length;
|
||||
final localIndex = (position.dx / candleWidth).floor().clamp(0, visibleData.klines.length - 1);
|
||||
_crossLineIndex = visibleData.startIndex + localIndex;
|
||||
// 计算对应的K线索引(基于像素位置)
|
||||
final absoluteX = _scrollX + position.dx;
|
||||
final index = (absoluteX / _candleWidth).floor().clamp(0, widget.klines.length - 1);
|
||||
_crossLineIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
// 数据处理
|
||||
_VisibleData _getVisibleData() {
|
||||
if (widget.klines.isEmpty) {
|
||||
return _VisibleData(klines: [], startIndex: 0);
|
||||
// 数据处理 - 基于像素滚动计算可见数据
|
||||
_VisibleData _getVisibleData(double chartWidth) {
|
||||
if (widget.klines.isEmpty || chartWidth == 0) {
|
||||
return _VisibleData(klines: [], startIndex: 0, candleWidth: _candleWidth);
|
||||
}
|
||||
|
||||
final int maxStart = math.max(0, widget.klines.length - _visibleCandleCount);
|
||||
final int startIndex = _offsetX.floor().clamp(0, maxStart);
|
||||
final int endIndex = math.min(startIndex + _visibleCandleCount, widget.klines.length);
|
||||
// 根据滚动位置计算可见的K线范围
|
||||
final int startIndex = (_scrollX / _candleWidth).floor().clamp(0, widget.klines.length - 1);
|
||||
final int visibleCount = (chartWidth / _candleWidth).ceil() + 1; // +1 确保边缘K线可见
|
||||
final int endIndex = math.min(startIndex + visibleCount, widget.klines.length);
|
||||
|
||||
return _VisibleData(
|
||||
klines: widget.klines.sublist(startIndex, endIndex),
|
||||
startIndex: startIndex,
|
||||
candleWidth: _candleWidth,
|
||||
);
|
||||
}
|
||||
|
||||
int _getVisibleCount() {
|
||||
if (_chartWidth == 0) return 60;
|
||||
return (_chartWidth / _candleWidth).ceil() + 1;
|
||||
}
|
||||
|
||||
Map<int, List<double?>> _getVisibleMAData(Map<int, List<double?>> fullData, int startIndex) {
|
||||
final visibleCount = _getVisibleCount();
|
||||
final result = <int, List<double?>>{};
|
||||
for (final entry in fullData.entries) {
|
||||
final endIndex = math.min(startIndex + _visibleCandleCount, entry.value.length);
|
||||
final endIndex = math.min(startIndex + visibleCount, entry.value.length);
|
||||
if (startIndex < entry.value.length) {
|
||||
result[entry.key] = entry.value.sublist(startIndex, endIndex);
|
||||
}
|
||||
|
|
@ -526,9 +548,10 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
}
|
||||
|
||||
Map<String, List<double?>> _getVisibleBollData(Map<String, List<double?>> fullData, int startIndex) {
|
||||
final visibleCount = _getVisibleCount();
|
||||
final result = <String, List<double?>>{};
|
||||
for (final entry in fullData.entries) {
|
||||
final endIndex = math.min(startIndex + _visibleCandleCount, entry.value.length);
|
||||
final endIndex = math.min(startIndex + visibleCount, entry.value.length);
|
||||
if (startIndex < entry.value.length) {
|
||||
result[entry.key] = entry.value.sublist(startIndex, endIndex);
|
||||
}
|
||||
|
|
@ -537,9 +560,10 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
}
|
||||
|
||||
Map<String, List<double?>> _getVisibleMacdData(Map<String, List<double?>> fullData, int startIndex) {
|
||||
final visibleCount = _getVisibleCount();
|
||||
final result = <String, List<double?>>{};
|
||||
for (final entry in fullData.entries) {
|
||||
final endIndex = math.min(startIndex + _visibleCandleCount, entry.value.length);
|
||||
final endIndex = math.min(startIndex + visibleCount, entry.value.length);
|
||||
if (startIndex < entry.value.length) {
|
||||
result[entry.key] = entry.value.sublist(startIndex, endIndex);
|
||||
}
|
||||
|
|
@ -548,9 +572,10 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
}
|
||||
|
||||
Map<String, List<double?>> _getVisibleKdjData(Map<String, List<double?>> fullData, int startIndex) {
|
||||
final visibleCount = _getVisibleCount();
|
||||
final result = <String, List<double?>>{};
|
||||
for (final entry in fullData.entries) {
|
||||
final endIndex = math.min(startIndex + _visibleCandleCount, entry.value.length);
|
||||
final endIndex = math.min(startIndex + visibleCount, entry.value.length);
|
||||
if (startIndex < entry.value.length) {
|
||||
result[entry.key] = entry.value.sublist(startIndex, endIndex);
|
||||
}
|
||||
|
|
@ -559,7 +584,8 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
}
|
||||
|
||||
List<double?> _getVisibleRsiData(List<double?> fullData, int startIndex) {
|
||||
final endIndex = math.min(startIndex + _visibleCandleCount, fullData.length);
|
||||
final visibleCount = _getVisibleCount();
|
||||
final endIndex = math.min(startIndex + visibleCount, fullData.length);
|
||||
if (startIndex < fullData.length) {
|
||||
return fullData.sublist(startIndex, endIndex);
|
||||
}
|
||||
|
|
@ -686,6 +712,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
class _VisibleData {
|
||||
final List<Kline> klines;
|
||||
final int startIndex;
|
||||
final double candleWidth;
|
||||
|
||||
_VisibleData({required this.klines, required this.startIndex});
|
||||
_VisibleData({required this.klines, required this.startIndex, required this.candleWidth});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class KlineIndicatorPainter extends CustomPainter {
|
|||
final Map<String, List<double?>>? kdjData;
|
||||
final List<double?>? rsiData;
|
||||
final int crossLineIndex;
|
||||
final double? candleWidth;
|
||||
|
||||
static const Color _green = Color(0xFF10B981);
|
||||
static const Color _red = Color(0xFFEF4444);
|
||||
|
|
@ -34,6 +35,7 @@ class KlineIndicatorPainter extends CustomPainter {
|
|||
this.kdjData,
|
||||
this.rsiData,
|
||||
this.crossLineIndex = -1,
|
||||
this.candleWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -72,8 +74,8 @@ class KlineIndicatorPainter extends CustomPainter {
|
|||
if (crossLineIndex >= 0) {
|
||||
final dataLength = _getDataLength();
|
||||
if (dataLength > 0 && crossLineIndex < dataLength) {
|
||||
final candleWidth = chartWidth / dataLength;
|
||||
final x = leftPadding + crossLineIndex * candleWidth + candleWidth / 2;
|
||||
final actualCandleWidth = candleWidth ?? (chartWidth / dataLength);
|
||||
final x = leftPadding + crossLineIndex * actualCandleWidth + actualCandleWidth / 2;
|
||||
canvas.drawLine(
|
||||
Offset(x, 0),
|
||||
Offset(x, size.height),
|
||||
|
|
@ -146,14 +148,14 @@ class KlineIndicatorPainter extends CustomPainter {
|
|||
);
|
||||
|
||||
// 绘制MACD柱状图
|
||||
final candleWidth = chartWidth / macd.length;
|
||||
final barWidth = math.max(candleWidth * 0.6, 2.0);
|
||||
final actualCandleWidth = candleWidth ?? (chartWidth / macd.length);
|
||||
final barWidth = math.max(actualCandleWidth * 0.6, 2.0);
|
||||
|
||||
for (int i = 0; i < macd.length; i++) {
|
||||
if (macd[i] == null) continue;
|
||||
|
||||
final value = macd[i]!;
|
||||
final x = leftPadding + i * candleWidth + candleWidth / 2;
|
||||
final x = leftPadding + i * actualCandleWidth + actualCandleWidth / 2;
|
||||
final y = valueToY(value);
|
||||
|
||||
final color = value >= 0 ? _red : _green;
|
||||
|
|
@ -173,10 +175,10 @@ class KlineIndicatorPainter extends CustomPainter {
|
|||
}
|
||||
|
||||
// 绘制DIF线
|
||||
_drawIndicatorLine(canvas, dif, _difColor, leftPadding, candleWidth, valueToY);
|
||||
_drawIndicatorLine(canvas, dif, _difColor, leftPadding, actualCandleWidth, valueToY);
|
||||
|
||||
// 绘制DEA线
|
||||
_drawIndicatorLine(canvas, dea, _deaColor, leftPadding, candleWidth, valueToY);
|
||||
_drawIndicatorLine(canvas, dea, _deaColor, leftPadding, actualCandleWidth, valueToY);
|
||||
|
||||
// 绘制图例
|
||||
_drawMACDLegend(canvas, size, leftPadding, dif, dea, macd);
|
||||
|
|
@ -234,16 +236,16 @@ class KlineIndicatorPainter extends CustomPainter {
|
|||
}
|
||||
}
|
||||
|
||||
final candleWidth = chartWidth / k.length;
|
||||
final actualCandleWidth = candleWidth ?? (chartWidth / k.length);
|
||||
|
||||
// 绘制K线
|
||||
_drawIndicatorLine(canvas, k, _kColor, leftPadding, candleWidth, valueToY);
|
||||
_drawIndicatorLine(canvas, k, _kColor, leftPadding, actualCandleWidth, valueToY);
|
||||
|
||||
// 绘制D线
|
||||
_drawIndicatorLine(canvas, d, _dColor, leftPadding, candleWidth, valueToY);
|
||||
_drawIndicatorLine(canvas, d, _dColor, leftPadding, actualCandleWidth, valueToY);
|
||||
|
||||
// 绘制J线
|
||||
_drawIndicatorLine(canvas, j, _jColor, leftPadding, candleWidth, valueToY);
|
||||
_drawIndicatorLine(canvas, j, _jColor, leftPadding, actualCandleWidth, valueToY);
|
||||
|
||||
// 绘制图例
|
||||
_drawKDJLegend(canvas, size, leftPadding, k, d, j);
|
||||
|
|
@ -301,10 +303,10 @@ class KlineIndicatorPainter extends CustomPainter {
|
|||
oversoldPaint,
|
||||
);
|
||||
|
||||
final candleWidth = chartWidth / rsiData!.length;
|
||||
final actualCandleWidth = candleWidth ?? (chartWidth / rsiData!.length);
|
||||
|
||||
// 绘制RSI线
|
||||
_drawIndicatorLine(canvas, rsiData!, _rsiColor, leftPadding, candleWidth, valueToY);
|
||||
_drawIndicatorLine(canvas, rsiData!, _rsiColor, leftPadding, actualCandleWidth, valueToY);
|
||||
|
||||
// 绘制图例
|
||||
_drawRSILegend(canvas, size, leftPadding, rsiData!);
|
||||
|
|
@ -497,6 +499,7 @@ class KlineIndicatorPainter extends CustomPainter {
|
|||
oldDelegate.macdData != macdData ||
|
||||
oldDelegate.kdjData != kdjData ||
|
||||
oldDelegate.rsiData != rsiData ||
|
||||
oldDelegate.crossLineIndex != crossLineIndex;
|
||||
oldDelegate.crossLineIndex != crossLineIndex ||
|
||||
oldDelegate.candleWidth != candleWidth;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class KlinePainter extends CustomPainter {
|
|||
final Map<int, List<double?>>? emaData;
|
||||
final Map<String, List<double?>>? bollData;
|
||||
final int crossLineIndex;
|
||||
final double? candleWidth; // 可选的K线宽度,不传则自动计算
|
||||
|
||||
static const Color _green = Color(0xFF10B981);
|
||||
static const Color _red = Color(0xFFEF4444);
|
||||
|
|
@ -36,6 +37,7 @@ class KlinePainter extends CustomPainter {
|
|||
this.emaData,
|
||||
this.bollData,
|
||||
this.crossLineIndex = -1,
|
||||
this.candleWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -111,10 +113,10 @@ class KlinePainter extends CustomPainter {
|
|||
// 绘制网格和价格标签
|
||||
_drawGrid(canvas, size, minPrice, maxPrice, leftPadding, rightPadding, topPadding, chartHeight);
|
||||
|
||||
// 计算K线宽度
|
||||
final candleWidth = chartWidth / klines.length;
|
||||
final bodyWidth = math.max(candleWidth * 0.7, 2.0);
|
||||
final gap = candleWidth * 0.15;
|
||||
// 计算K线宽度 - 使用传入的宽度或自动计算
|
||||
final actualCandleWidth = candleWidth ?? (chartWidth / klines.length);
|
||||
final bodyWidth = math.max(actualCandleWidth * 0.7, 2.0);
|
||||
final gap = actualCandleWidth * 0.15;
|
||||
|
||||
// 绘制K线
|
||||
for (int i = 0; i < klines.length; i++) {
|
||||
|
|
@ -128,7 +130,7 @@ class KlinePainter extends CustomPainter {
|
|||
final color = isUp ? _green : _red;
|
||||
final paint = Paint()..color = color;
|
||||
|
||||
final x = leftPadding + i * candleWidth + candleWidth / 2;
|
||||
final x = leftPadding + i * actualCandleWidth + actualCandleWidth / 2;
|
||||
final yOpen = priceToY(open);
|
||||
final yClose = priceToY(close);
|
||||
final yHigh = priceToY(high);
|
||||
|
|
@ -170,7 +172,7 @@ class KlinePainter extends CustomPainter {
|
|||
entry.value,
|
||||
_maColors[colorIndex % _maColors.length],
|
||||
leftPadding,
|
||||
candleWidth,
|
||||
actualCandleWidth,
|
||||
priceToY,
|
||||
);
|
||||
colorIndex++;
|
||||
|
|
@ -186,7 +188,7 @@ class KlinePainter extends CustomPainter {
|
|||
entry.value,
|
||||
_maColors[colorIndex % _maColors.length],
|
||||
leftPadding,
|
||||
candleWidth,
|
||||
actualCandleWidth,
|
||||
priceToY,
|
||||
);
|
||||
colorIndex++;
|
||||
|
|
@ -195,14 +197,14 @@ class KlinePainter extends CustomPainter {
|
|||
|
||||
// 绘制BOLL线
|
||||
if (bollData != null) {
|
||||
_drawLine(canvas, bollData!['middle']!, _bollMiddleColor, leftPadding, candleWidth, priceToY);
|
||||
_drawLine(canvas, bollData!['upper']!, _bollUpperColor, leftPadding, candleWidth, priceToY, isDashed: true);
|
||||
_drawLine(canvas, bollData!['lower']!, _bollLowerColor, leftPadding, candleWidth, priceToY, isDashed: true);
|
||||
_drawLine(canvas, bollData!['middle']!, _bollMiddleColor, leftPadding, actualCandleWidth, priceToY);
|
||||
_drawLine(canvas, bollData!['upper']!, _bollUpperColor, leftPadding, actualCandleWidth, priceToY, isDashed: true);
|
||||
_drawLine(canvas, bollData!['lower']!, _bollLowerColor, leftPadding, actualCandleWidth, priceToY, isDashed: true);
|
||||
}
|
||||
|
||||
// 绘制十字线
|
||||
if (crossLineIndex >= 0 && crossLineIndex < klines.length) {
|
||||
_drawCrossLine(canvas, size, crossLineIndex, leftPadding, candleWidth, priceToY);
|
||||
_drawCrossLine(canvas, size, crossLineIndex, leftPadding, actualCandleWidth, priceToY);
|
||||
}
|
||||
|
||||
// 绘制MA图例
|
||||
|
|
@ -432,6 +434,7 @@ class KlinePainter extends CustomPainter {
|
|||
oldDelegate.maData != maData ||
|
||||
oldDelegate.emaData != emaData ||
|
||||
oldDelegate.bollData != bollData ||
|
||||
oldDelegate.crossLineIndex != crossLineIndex;
|
||||
oldDelegate.crossLineIndex != crossLineIndex ||
|
||||
oldDelegate.candleWidth != candleWidth;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import '../../../domain/entities/kline.dart';
|
|||
class KlineVolumePainter extends CustomPainter {
|
||||
final List<Kline> klines;
|
||||
final int crossLineIndex;
|
||||
final double? candleWidth;
|
||||
|
||||
static const Color _green = Color(0xFF10B981);
|
||||
static const Color _red = Color(0xFFEF4444);
|
||||
|
|
@ -16,6 +17,7 @@ class KlineVolumePainter extends CustomPainter {
|
|||
KlineVolumePainter({
|
||||
required this.klines,
|
||||
this.crossLineIndex = -1,
|
||||
this.candleWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -72,8 +74,8 @@ class KlineVolumePainter extends CustomPainter {
|
|||
maxVolText.paint(canvas, Offset(size.width - rightPadding + 4, topPadding));
|
||||
|
||||
// 绘制成交量柱
|
||||
final candleWidth = chartWidth / klines.length;
|
||||
final barWidth = math.max(candleWidth * 0.7, 2.0);
|
||||
final actualCandleWidth = candleWidth ?? (chartWidth / klines.length);
|
||||
final barWidth = math.max(actualCandleWidth * 0.7, 2.0);
|
||||
|
||||
for (int i = 0; i < klines.length; i++) {
|
||||
final kline = klines[i];
|
||||
|
|
@ -84,7 +86,7 @@ class KlineVolumePainter extends CustomPainter {
|
|||
final isUp = close >= open;
|
||||
final color = isUp ? _green : _red;
|
||||
|
||||
final x = leftPadding + i * candleWidth + candleWidth / 2;
|
||||
final x = leftPadding + i * actualCandleWidth + actualCandleWidth / 2;
|
||||
final barHeight = (volume / maxVolume) * chartHeight;
|
||||
final y = size.height - bottomPadding - barHeight;
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ class KlineVolumePainter extends CustomPainter {
|
|||
|
||||
// 绘制十字线
|
||||
if (crossLineIndex >= 0 && crossLineIndex < klines.length) {
|
||||
final x = leftPadding + crossLineIndex * candleWidth + candleWidth / 2;
|
||||
final x = leftPadding + crossLineIndex * actualCandleWidth + actualCandleWidth / 2;
|
||||
canvas.drawLine(
|
||||
Offset(x, 0),
|
||||
Offset(x, size.height),
|
||||
|
|
@ -131,6 +133,8 @@ class KlineVolumePainter extends CustomPainter {
|
|||
|
||||
@override
|
||||
bool shouldRepaint(covariant KlineVolumePainter oldDelegate) {
|
||||
return oldDelegate.klines != klines || oldDelegate.crossLineIndex != crossLineIndex;
|
||||
return oldDelegate.klines != klines ||
|
||||
oldDelegate.crossLineIndex != crossLineIndex ||
|
||||
oldDelegate.candleWidth != candleWidth;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue