fix(kline): 稀疏K线数据时居中显示
当K线数量较少(如1分钟图刚开始)时,K线不再拉伸填满屏幕, 而是使用默认宽度并在可用区域内水平居中显示。 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f51aa44cd9
commit
f1b2ec7ede
|
|
@ -79,12 +79,17 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
||||||
|
|
||||||
// 计算让所有 K 线占满屏幕的宽度
|
// 计算让所有 K 线占满屏幕的宽度
|
||||||
final idealWidth = chartWidth / widget.klines.length;
|
final idealWidth = chartWidth / widget.klines.length;
|
||||||
// 限制在合理范围内
|
// 限制在合理范围内,但如果数据太少,使用默认宽度而不是拉伸
|
||||||
_candleWidth = idealWidth.clamp(_minCandleWidth, _maxCandleWidth);
|
if (idealWidth > _maxCandleWidth) {
|
||||||
|
// K 线数量少,使用默认宽度(不要拉伸太宽)
|
||||||
|
_candleWidth = 8.0;
|
||||||
|
} else {
|
||||||
|
_candleWidth = idealWidth.clamp(_minCandleWidth, _maxCandleWidth);
|
||||||
|
}
|
||||||
_prevCandleWidth = _candleWidth;
|
_prevCandleWidth = _candleWidth;
|
||||||
|
|
||||||
// 首次加载时让最新 K 线居中显示
|
// 首次加载时让最新 K 线靠右显示
|
||||||
_scrollToCenter();
|
_scrollToEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 滚动使最新 K 线居中显示
|
/// 滚动使最新 K 线居中显示
|
||||||
|
|
@ -390,6 +395,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
||||||
crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1,
|
crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1,
|
||||||
candleWidth: visibleData.candleWidth,
|
candleWidth: visibleData.candleWidth,
|
||||||
isDark: AppColors.isDark(context),
|
isDark: AppColors.isDark(context),
|
||||||
|
leftOffset: visibleData.leftOffset,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 十字线信息
|
// 十字线信息
|
||||||
|
|
@ -430,6 +436,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
||||||
crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1,
|
crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1,
|
||||||
candleWidth: visibleData.candleWidth,
|
candleWidth: visibleData.candleWidth,
|
||||||
isDark: AppColors.isDark(context),
|
isDark: AppColors.isDark(context),
|
||||||
|
leftOffset: visibleData.leftOffset,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -570,6 +577,16 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
||||||
return _VisibleData(klines: [], startIndex: 0, candleWidth: _candleWidth);
|
return _VisibleData(klines: [], startIndex: 0, candleWidth: _candleWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算K线总宽度(不包含左右padding)
|
||||||
|
const rightPadding = 50.0; // 与 painter 中的 rightPadding 保持一致
|
||||||
|
const leftPadding = 8.0; // 与 painter 中的 leftPadding 保持一致
|
||||||
|
final availableWidth = chartWidth - rightPadding - leftPadding;
|
||||||
|
final totalWidth = widget.klines.length * _candleWidth;
|
||||||
|
|
||||||
|
// 计算左侧偏移量:当K线总宽度小于可用宽度时,将K线居中显示
|
||||||
|
// 居中偏移 = (可用宽度 - K线总宽度) / 2
|
||||||
|
final leftOffset = totalWidth < availableWidth ? (availableWidth - totalWidth) / 2 : 0.0;
|
||||||
|
|
||||||
// 根据滚动位置计算可见的K线范围
|
// 根据滚动位置计算可见的K线范围
|
||||||
final int startIndex = (_scrollX / _candleWidth).floor().clamp(0, widget.klines.length - 1);
|
final int startIndex = (_scrollX / _candleWidth).floor().clamp(0, widget.klines.length - 1);
|
||||||
final int visibleCount = (chartWidth / _candleWidth).ceil() + 1; // +1 确保边缘K线可见
|
final int visibleCount = (chartWidth / _candleWidth).ceil() + 1; // +1 确保边缘K线可见
|
||||||
|
|
@ -579,6 +596,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
||||||
klines: widget.klines.sublist(startIndex, endIndex),
|
klines: widget.klines.sublist(startIndex, endIndex),
|
||||||
startIndex: startIndex,
|
startIndex: startIndex,
|
||||||
candleWidth: _candleWidth,
|
candleWidth: _candleWidth,
|
||||||
|
leftOffset: leftOffset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -771,6 +789,12 @@ class _VisibleData {
|
||||||
final List<Kline> klines;
|
final List<Kline> klines;
|
||||||
final int startIndex;
|
final int startIndex;
|
||||||
final double candleWidth;
|
final double candleWidth;
|
||||||
|
final double leftOffset; // K线数据稀疏时的左侧偏移,用于右对齐显示
|
||||||
|
|
||||||
_VisibleData({required this.klines, required this.startIndex, required this.candleWidth});
|
_VisibleData({
|
||||||
|
required this.klines,
|
||||||
|
required this.startIndex,
|
||||||
|
required this.candleWidth,
|
||||||
|
this.leftOffset = 0.0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ class KlinePainter extends CustomPainter {
|
||||||
final int crossLineIndex;
|
final int crossLineIndex;
|
||||||
final double? candleWidth; // 可选的K线宽度,不传则自动计算
|
final double? candleWidth; // 可选的K线宽度,不传则自动计算
|
||||||
final bool isDark; // 深色模式
|
final bool isDark; // 深色模式
|
||||||
|
final double leftOffset; // K线稀疏时的左侧偏移,用于右对齐显示
|
||||||
|
|
||||||
static const Color _green = Color(0xFF10B981);
|
static const Color _green = Color(0xFF10B981);
|
||||||
static const Color _red = Color(0xFFEF4444);
|
static const Color _red = Color(0xFFEF4444);
|
||||||
|
|
@ -42,6 +43,7 @@ class KlinePainter extends CustomPainter {
|
||||||
this.crossLineIndex = -1,
|
this.crossLineIndex = -1,
|
||||||
this.candleWidth,
|
this.candleWidth,
|
||||||
this.isDark = false,
|
this.isDark = false,
|
||||||
|
this.leftOffset = 0.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -137,7 +139,7 @@ class KlinePainter extends CustomPainter {
|
||||||
final color = isUp ? _green : _red;
|
final color = isUp ? _green : _red;
|
||||||
final paint = Paint()..color = color;
|
final paint = Paint()..color = color;
|
||||||
|
|
||||||
final x = leftPadding + i * actualCandleWidth + actualCandleWidth / 2;
|
final x = leftPadding + leftOffset + i * actualCandleWidth + actualCandleWidth / 2;
|
||||||
final yOpen = priceToY(open);
|
final yOpen = priceToY(open);
|
||||||
final yClose = priceToY(close);
|
final yClose = priceToY(close);
|
||||||
final yHigh = priceToY(high);
|
final yHigh = priceToY(high);
|
||||||
|
|
@ -178,7 +180,7 @@ class KlinePainter extends CustomPainter {
|
||||||
canvas,
|
canvas,
|
||||||
entry.value,
|
entry.value,
|
||||||
_maColors[colorIndex % _maColors.length],
|
_maColors[colorIndex % _maColors.length],
|
||||||
leftPadding,
|
leftPadding + leftOffset,
|
||||||
actualCandleWidth,
|
actualCandleWidth,
|
||||||
priceToY,
|
priceToY,
|
||||||
);
|
);
|
||||||
|
|
@ -194,7 +196,7 @@ class KlinePainter extends CustomPainter {
|
||||||
canvas,
|
canvas,
|
||||||
entry.value,
|
entry.value,
|
||||||
_maColors[colorIndex % _maColors.length],
|
_maColors[colorIndex % _maColors.length],
|
||||||
leftPadding,
|
leftPadding + leftOffset,
|
||||||
actualCandleWidth,
|
actualCandleWidth,
|
||||||
priceToY,
|
priceToY,
|
||||||
);
|
);
|
||||||
|
|
@ -204,14 +206,14 @@ class KlinePainter extends CustomPainter {
|
||||||
|
|
||||||
// 绘制BOLL线
|
// 绘制BOLL线
|
||||||
if (bollData != null) {
|
if (bollData != null) {
|
||||||
_drawLine(canvas, bollData!['middle']!, _bollMiddleColor, leftPadding, actualCandleWidth, priceToY);
|
_drawLine(canvas, bollData!['middle']!, _bollMiddleColor, leftPadding + leftOffset, actualCandleWidth, priceToY);
|
||||||
_drawLine(canvas, bollData!['upper']!, _bollUpperColor, leftPadding, actualCandleWidth, priceToY, isDashed: true);
|
_drawLine(canvas, bollData!['upper']!, _bollUpperColor, leftPadding + leftOffset, actualCandleWidth, priceToY, isDashed: true);
|
||||||
_drawLine(canvas, bollData!['lower']!, _bollLowerColor, leftPadding, actualCandleWidth, priceToY, isDashed: true);
|
_drawLine(canvas, bollData!['lower']!, _bollLowerColor, leftPadding + leftOffset, actualCandleWidth, priceToY, isDashed: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制十字线
|
// 绘制十字线
|
||||||
if (crossLineIndex >= 0 && crossLineIndex < klines.length) {
|
if (crossLineIndex >= 0 && crossLineIndex < klines.length) {
|
||||||
_drawCrossLine(canvas, size, crossLineIndex, leftPadding, actualCandleWidth, priceToY);
|
_drawCrossLine(canvas, size, crossLineIndex, leftPadding + leftOffset, actualCandleWidth, priceToY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制MA图例
|
// 绘制MA图例
|
||||||
|
|
@ -452,6 +454,7 @@ class KlinePainter extends CustomPainter {
|
||||||
oldDelegate.bollData != bollData ||
|
oldDelegate.bollData != bollData ||
|
||||||
oldDelegate.crossLineIndex != crossLineIndex ||
|
oldDelegate.crossLineIndex != crossLineIndex ||
|
||||||
oldDelegate.candleWidth != candleWidth ||
|
oldDelegate.candleWidth != candleWidth ||
|
||||||
oldDelegate.isDark != isDark;
|
oldDelegate.isDark != isDark ||
|
||||||
|
oldDelegate.leftOffset != leftOffset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ class KlineVolumePainter extends CustomPainter {
|
||||||
final int crossLineIndex;
|
final int crossLineIndex;
|
||||||
final double? candleWidth;
|
final double? candleWidth;
|
||||||
final bool isDark; // 深色模式
|
final bool isDark; // 深色模式
|
||||||
|
final double leftOffset; // K线稀疏时的左侧偏移,用于右对齐显示
|
||||||
|
|
||||||
static const Color _green = Color(0xFF10B981);
|
static const Color _green = Color(0xFF10B981);
|
||||||
static const Color _red = Color(0xFFEF4444);
|
static const Color _red = Color(0xFFEF4444);
|
||||||
|
|
@ -23,6 +24,7 @@ class KlineVolumePainter extends CustomPainter {
|
||||||
this.crossLineIndex = -1,
|
this.crossLineIndex = -1,
|
||||||
this.candleWidth,
|
this.candleWidth,
|
||||||
this.isDark = false,
|
this.isDark = false,
|
||||||
|
this.leftOffset = 0.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -91,7 +93,7 @@ class KlineVolumePainter extends CustomPainter {
|
||||||
final isUp = close >= open;
|
final isUp = close >= open;
|
||||||
final color = isUp ? _green : _red;
|
final color = isUp ? _green : _red;
|
||||||
|
|
||||||
final x = leftPadding + i * actualCandleWidth + actualCandleWidth / 2;
|
final x = leftPadding + leftOffset + i * actualCandleWidth + actualCandleWidth / 2;
|
||||||
final barHeight = (volume / maxVolume) * chartHeight;
|
final barHeight = (volume / maxVolume) * chartHeight;
|
||||||
final y = size.height - bottomPadding - barHeight;
|
final y = size.height - bottomPadding - barHeight;
|
||||||
|
|
||||||
|
|
@ -106,7 +108,7 @@ class KlineVolumePainter extends CustomPainter {
|
||||||
|
|
||||||
// 绘制十字线
|
// 绘制十字线
|
||||||
if (crossLineIndex >= 0 && crossLineIndex < klines.length) {
|
if (crossLineIndex >= 0 && crossLineIndex < klines.length) {
|
||||||
final x = leftPadding + crossLineIndex * actualCandleWidth + actualCandleWidth / 2;
|
final x = leftPadding + leftOffset + crossLineIndex * actualCandleWidth + actualCandleWidth / 2;
|
||||||
canvas.drawLine(
|
canvas.drawLine(
|
||||||
Offset(x, 0),
|
Offset(x, 0),
|
||||||
Offset(x, size.height),
|
Offset(x, size.height),
|
||||||
|
|
@ -141,6 +143,7 @@ class KlineVolumePainter extends CustomPainter {
|
||||||
return oldDelegate.klines != klines ||
|
return oldDelegate.klines != klines ||
|
||||||
oldDelegate.crossLineIndex != crossLineIndex ||
|
oldDelegate.crossLineIndex != crossLineIndex ||
|
||||||
oldDelegate.candleWidth != candleWidth ||
|
oldDelegate.candleWidth != candleWidth ||
|
||||||
oldDelegate.isDark != isDark;
|
oldDelegate.isDark != isDark ||
|
||||||
|
oldDelegate.leftOffset != leftOffset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue