feat(kline): implement true pan effect with scrollOffset
- Added scrollOffset parameter to KlinePainter and KlineVolumePainter - Painters now draw all klines and apply scrollOffset for positioning - Added canvas clipping to prevent drawing outside chart bounds - Removed _getVisibleData and related helper methods (no longer needed) - scrollOffset directly controls the visual position of all klines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
48ba72ce89
commit
f55fb13f26
|
|
@ -94,23 +94,29 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
///
|
||||
/// 计算逻辑:
|
||||
/// - K线从左边开始排列
|
||||
/// - 数据不够时(总宽度 <= 屏幕宽度的一半),不滚动,有多少显示多少
|
||||
/// - 数据不够时,不滚动,有多少显示多少
|
||||
/// - 数据量大时,计算滚动位置让最新K线在屏幕中心
|
||||
void _scrollToCenter() {
|
||||
if (widget.klines.isEmpty || _chartWidth == 0) return;
|
||||
|
||||
// 计算屏幕中心到左边缘能显示多少根K线
|
||||
final int halfScreenCount = (_chartWidth / 2 / _candleWidth).ceil();
|
||||
// painter 内部的 padding 常量
|
||||
const leftPadding = 8.0;
|
||||
const rightPadding = 50.0;
|
||||
final drawableWidth = _chartWidth - leftPadding - rightPadding;
|
||||
|
||||
if (widget.klines.length <= halfScreenCount) {
|
||||
// 数据不够半屏,不滚动,从左开始显示
|
||||
// 所有K线的总宽度
|
||||
final totalKlineWidth = widget.klines.length * _candleWidth;
|
||||
|
||||
if (totalKlineWidth <= drawableWidth) {
|
||||
// 数据不够填满可绘制区域,不滚动
|
||||
_scrollX = 0;
|
||||
} else {
|
||||
// 数据量足够,滚动使最新K线在屏幕中心
|
||||
// startIndex = klines.length - halfScreenCount
|
||||
// scrollX = startIndex * candleWidth
|
||||
final int startIndex = widget.klines.length - halfScreenCount;
|
||||
_scrollX = startIndex * _candleWidth;
|
||||
// 最新K线的中心位置(从leftPadding开始)= (N-0.5) * candleWidth
|
||||
// 目标:让这个位置对齐到 drawableWidth/2
|
||||
// scrollOffset = (N-0.5) * candleWidth - drawableWidth/2
|
||||
_scrollX = (widget.klines.length - 0.5) * _candleWidth - drawableWidth / 2;
|
||||
_scrollX = math.max(0.0, _scrollX);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -353,9 +359,6 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
// 计算可见数据(基于像素滚动)
|
||||
final visibleData = _getVisibleData(chartWidth);
|
||||
|
||||
// 计算指标数据
|
||||
final processor = KlineDataProcessor(widget.klines);
|
||||
final maData = processor.calculateMA([5, 10, 20, 60]);
|
||||
|
|
@ -386,24 +389,24 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
CustomPaint(
|
||||
size: Size(chartWidth, mainChartHeight),
|
||||
painter: KlinePainter(
|
||||
klines: visibleData.klines,
|
||||
maData: _selectedMainIndicator == 0 ? _getVisibleMAData(maData, visibleData.startIndex) : null,
|
||||
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,
|
||||
klines: widget.klines,
|
||||
maData: _selectedMainIndicator == 0 ? maData : null,
|
||||
emaData: _selectedMainIndicator == 1 ? emaData : null,
|
||||
bollData: _selectedMainIndicator == 2 ? bollData : null,
|
||||
crossLineIndex: _showCrossLine ? _crossLineIndex : -1,
|
||||
candleWidth: _candleWidth,
|
||||
isDark: AppColors.isDark(context),
|
||||
scrollOffset: _scrollX,
|
||||
),
|
||||
),
|
||||
// 十字线信息
|
||||
if (_showCrossLine && _crossLineIndex >= 0 && _crossLineIndex < widget.klines.length)
|
||||
_buildCrossLineInfo(visibleData),
|
||||
_buildCrossLineInfo(),
|
||||
// 当前价格标签
|
||||
if (visibleData.klines.isNotEmpty)
|
||||
if (widget.klines.isNotEmpty)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: _calcPriceY(
|
||||
visibleData,
|
||||
double.tryParse(widget.currentPrice) ?? 0,
|
||||
mainChartHeight,
|
||||
),
|
||||
|
|
@ -429,10 +432,11 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
child: CustomPaint(
|
||||
size: Size(chartWidth, volumeHeight),
|
||||
painter: KlineVolumePainter(
|
||||
klines: visibleData.klines,
|
||||
crossLineIndex: _showCrossLine ? _crossLineIndex - visibleData.startIndex : -1,
|
||||
candleWidth: visibleData.candleWidth,
|
||||
klines: widget.klines,
|
||||
crossLineIndex: _showCrossLine ? _crossLineIndex : -1,
|
||||
candleWidth: _candleWidth,
|
||||
isDark: AppColors.isDark(context),
|
||||
scrollOffset: _scrollX,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -443,11 +447,12 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
// size: Size(chartWidth, indicatorHeight),
|
||||
// painter: KlineIndicatorPainter(
|
||||
// indicatorType: _selectedSubIndicator,
|
||||
// macdData: _selectedSubIndicator == 0 ? _getVisibleMacdData(macdData, visibleData.startIndex) : null,
|
||||
// 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,
|
||||
// macdData: _selectedSubIndicator == 0 ? macdData : null,
|
||||
// kdjData: _selectedSubIndicator == 1 ? kdjData : null,
|
||||
// rsiData: _selectedSubIndicator == 2 ? rsiData : null,
|
||||
// crossLineIndex: _showCrossLine ? _crossLineIndex : -1,
|
||||
// candleWidth: _candleWidth,
|
||||
// scrollOffset: _scrollX,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
|
|
@ -505,6 +510,11 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
|
||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
setState(() {
|
||||
// painter 内部的 padding 常量
|
||||
const leftPadding = 8.0;
|
||||
const rightPadding = 50.0;
|
||||
final drawableWidth = _chartWidth - leftPadding - rightPadding;
|
||||
|
||||
// 两指缩放
|
||||
if (_isScaling) {
|
||||
final newCandleWidth = (_prevCandleWidth * details.scale).clamp(_minCandleWidth, _maxCandleWidth);
|
||||
|
|
@ -514,8 +524,8 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
final oldFocalIndex = (_startScrollX + _startFocalPoint!.dx) / _prevCandleWidth;
|
||||
final newFocalX = oldFocalIndex * newCandleWidth;
|
||||
// 缩放后重新计算maxScroll
|
||||
final int halfScreenCount = (_chartWidth / 2 / newCandleWidth).ceil();
|
||||
final double maxScroll = math.max(0.0, (widget.klines.length - halfScreenCount) * newCandleWidth);
|
||||
final newDrawableWidth = _chartWidth - leftPadding - rightPadding;
|
||||
final double maxScroll = math.max(0.0, (widget.klines.length - 0.5) * newCandleWidth - newDrawableWidth / 2);
|
||||
_scrollX = (newFocalX - focalRatio * _chartWidth).clamp(0.0, maxScroll);
|
||||
}
|
||||
|
||||
|
|
@ -525,13 +535,12 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
// dx > 0 向右滑(看更早历史),dx < 0 向左滑(看更新数据)
|
||||
final dx = details.focalPoint.dx - _startFocalPoint!.dx;
|
||||
// 最大滚动位置:让最新K线在屏幕中心
|
||||
final int halfScreenCount = (_chartWidth / 2 / _candleWidth).ceil();
|
||||
final double maxScroll = math.max(0.0, (widget.klines.length - halfScreenCount) * _candleWidth);
|
||||
final double maxScroll = math.max(0.0, (widget.klines.length - 0.5) * _candleWidth - drawableWidth / 2);
|
||||
// 向右滑 dx>0,scrollX 减小,显示更早的数据
|
||||
_scrollX = (_startScrollX - dx).clamp(0.0, maxScroll);
|
||||
|
||||
// DEBUG: 打印滚动信息
|
||||
debugPrint('Pan: dx=$dx, scrollX=$_scrollX, maxScroll=$maxScroll, startIndex=${(_scrollX / _candleWidth).floor()}');
|
||||
debugPrint('Pan: dx=$dx, scrollX=$_scrollX, maxScroll=$maxScroll');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -564,106 +573,17 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
_showCrossLine = true;
|
||||
|
||||
// 计算对应的K线索引(基于像素位置)
|
||||
final absoluteX = _scrollX + position.dx;
|
||||
final index = (absoluteX / _candleWidth).floor().clamp(0, widget.klines.length - 1);
|
||||
// position.dx 是相对于widget的位置
|
||||
// painter 中第i根K线的x坐标 = leftPadding + i * candleWidth + candleWidth/2 - scrollOffset
|
||||
// 所以 i = (position.dx + scrollOffset - leftPadding - candleWidth/2) / candleWidth
|
||||
// i = (position.dx + scrollOffset - leftPadding) / candleWidth - 0.5
|
||||
const leftPadding = 8.0;
|
||||
final index = ((position.dx + _scrollX - leftPadding) / _candleWidth).floor().clamp(0, widget.klines.length - 1);
|
||||
_crossLineIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
// 数据处理 - 计算要显示的K线数据
|
||||
_VisibleData _getVisibleData(double chartWidth) {
|
||||
if (widget.klines.isEmpty || chartWidth == 0) {
|
||||
return _VisibleData(klines: [], startIndex: 0, candleWidth: _candleWidth);
|
||||
}
|
||||
|
||||
// 计算屏幕能显示多少根K线
|
||||
final int fullScreenCount = (chartWidth / _candleWidth).ceil() + 1;
|
||||
// 计算屏幕中心到左边缘能显示多少根K线
|
||||
final int halfScreenCount = (chartWidth / 2 / _candleWidth).ceil();
|
||||
|
||||
// 数据量不够半屏时,显示全部,从左开始
|
||||
if (widget.klines.length <= halfScreenCount) {
|
||||
return _VisibleData(
|
||||
klines: widget.klines,
|
||||
startIndex: 0,
|
||||
candleWidth: _candleWidth,
|
||||
);
|
||||
}
|
||||
|
||||
// 数据量足够时,根据 _scrollX 计算起始索引
|
||||
final int startIndex = (_scrollX / _candleWidth).floor().clamp(0, math.max(0, widget.klines.length - 1));
|
||||
final int endIndex = math.min(startIndex + fullScreenCount, 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 + visibleCount, entry.value.length);
|
||||
if (startIndex < entry.value.length) {
|
||||
result[entry.key] = entry.value.sublist(startIndex, endIndex);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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 + visibleCount, entry.value.length);
|
||||
if (startIndex < entry.value.length) {
|
||||
result[entry.key] = entry.value.sublist(startIndex, endIndex);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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 + visibleCount, entry.value.length);
|
||||
if (startIndex < entry.value.length) {
|
||||
result[entry.key] = entry.value.sublist(startIndex, endIndex);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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 + visibleCount, entry.value.length);
|
||||
if (startIndex < entry.value.length) {
|
||||
result[entry.key] = entry.value.sublist(startIndex, endIndex);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<double?> _getVisibleRsiData(List<double?> fullData, int startIndex) {
|
||||
final visibleCount = _getVisibleCount();
|
||||
final endIndex = math.min(startIndex + visibleCount, fullData.length);
|
||||
if (startIndex < fullData.length) {
|
||||
return fullData.sublist(startIndex, endIndex);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Widget _buildCrossLineInfo(_VisibleData visibleData) {
|
||||
Widget _buildCrossLineInfo() {
|
||||
if (_crossLineIndex < 0 || _crossLineIndex >= widget.klines.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
|
@ -759,12 +679,12 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
return '${time.month}/${time.day} ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
double _calcPriceY(_VisibleData visibleData, double price, double chartHeight) {
|
||||
if (visibleData.klines.isEmpty) return chartHeight / 2;
|
||||
double _calcPriceY(double price, double chartHeight) {
|
||||
if (widget.klines.isEmpty) return chartHeight / 2;
|
||||
|
||||
double minPrice = double.infinity;
|
||||
double maxPrice = double.negativeInfinity;
|
||||
for (final kline in visibleData.klines) {
|
||||
for (final kline in widget.klines) {
|
||||
final low = double.tryParse(kline.low) ?? 0;
|
||||
final high = double.tryParse(kline.high) ?? 0;
|
||||
if (low < minPrice) minPrice = low;
|
||||
|
|
@ -785,11 +705,3 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
return y.clamp(0.0, chartHeight - 20);
|
||||
}
|
||||
}
|
||||
|
||||
class _VisibleData {
|
||||
final List<Kline> klines;
|
||||
final int startIndex;
|
||||
final double candleWidth;
|
||||
|
||||
_VisibleData({required this.klines, required this.startIndex, required this.candleWidth});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class KlinePainter extends CustomPainter {
|
|||
final int crossLineIndex;
|
||||
final double? candleWidth; // 可选的K线宽度,不传则自动计算
|
||||
final bool isDark; // 深色模式
|
||||
final double scrollOffset; // 滚动偏移(像素)
|
||||
|
||||
static const Color _green = Color(0xFF10B981);
|
||||
static const Color _red = Color(0xFFEF4444);
|
||||
|
|
@ -42,6 +43,7 @@ class KlinePainter extends CustomPainter {
|
|||
this.crossLineIndex = -1,
|
||||
this.candleWidth,
|
||||
this.isDark = false,
|
||||
this.scrollOffset = 0.0,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -123,7 +125,10 @@ class KlinePainter extends CustomPainter {
|
|||
// 计算K线宽度 - 使用传入的宽度或自动计算
|
||||
final actualCandleWidth = candleWidth ?? (chartWidth / klines.length);
|
||||
final bodyWidth = math.max(actualCandleWidth * 0.7, 2.0);
|
||||
final gap = actualCandleWidth * 0.15;
|
||||
|
||||
// 裁剪绘图区域,防止K线超出边界
|
||||
canvas.save();
|
||||
canvas.clipRect(Rect.fromLTRB(leftPadding, 0, size.width - rightPadding, size.height));
|
||||
|
||||
// 绘制K线
|
||||
for (int i = 0; i < klines.length; i++) {
|
||||
|
|
@ -137,7 +142,14 @@ class KlinePainter extends CustomPainter {
|
|||
final color = isUp ? _green : _red;
|
||||
final paint = Paint()..color = color;
|
||||
|
||||
final x = leftPadding + i * actualCandleWidth + actualCandleWidth / 2;
|
||||
// 应用滚动偏移:scrollOffset为正时,K线向左移动
|
||||
final x = leftPadding + i * actualCandleWidth + actualCandleWidth / 2 - scrollOffset;
|
||||
|
||||
// 跳过不在可见区域的K线(优化性能)
|
||||
if (x < leftPadding - actualCandleWidth || x > size.width - rightPadding + actualCandleWidth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final yOpen = priceToY(open);
|
||||
final yClose = priceToY(close);
|
||||
final yHigh = priceToY(high);
|
||||
|
|
@ -170,7 +182,12 @@ class KlinePainter extends CustomPainter {
|
|||
}
|
||||
}
|
||||
|
||||
// 绘制MA线
|
||||
canvas.restore();
|
||||
|
||||
// 绘制MA线(在裁剪区域内)
|
||||
canvas.save();
|
||||
canvas.clipRect(Rect.fromLTRB(leftPadding, 0, size.width - rightPadding, size.height));
|
||||
|
||||
if (maData != null) {
|
||||
int colorIndex = 0;
|
||||
for (final entry in maData!.entries) {
|
||||
|
|
@ -181,6 +198,7 @@ class KlinePainter extends CustomPainter {
|
|||
leftPadding,
|
||||
actualCandleWidth,
|
||||
priceToY,
|
||||
scrollOffset,
|
||||
);
|
||||
colorIndex++;
|
||||
}
|
||||
|
|
@ -197,6 +215,7 @@ class KlinePainter extends CustomPainter {
|
|||
leftPadding,
|
||||
actualCandleWidth,
|
||||
priceToY,
|
||||
scrollOffset,
|
||||
);
|
||||
colorIndex++;
|
||||
}
|
||||
|
|
@ -204,14 +223,16 @@ class KlinePainter extends CustomPainter {
|
|||
|
||||
// 绘制BOLL线
|
||||
if (bollData != null) {
|
||||
_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);
|
||||
_drawLine(canvas, bollData!['middle']!, _bollMiddleColor, leftPadding, actualCandleWidth, priceToY, scrollOffset);
|
||||
_drawLine(canvas, bollData!['upper']!, _bollUpperColor, leftPadding, actualCandleWidth, priceToY, scrollOffset, isDashed: true);
|
||||
_drawLine(canvas, bollData!['lower']!, _bollLowerColor, leftPadding, actualCandleWidth, priceToY, scrollOffset, isDashed: true);
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
|
||||
// 绘制十字线
|
||||
if (crossLineIndex >= 0 && crossLineIndex < klines.length) {
|
||||
_drawCrossLine(canvas, size, crossLineIndex, leftPadding, actualCandleWidth, priceToY);
|
||||
_drawCrossLine(canvas, size, crossLineIndex, leftPadding, actualCandleWidth, priceToY, scrollOffset);
|
||||
}
|
||||
|
||||
// 绘制MA图例
|
||||
|
|
@ -265,7 +286,8 @@ class KlinePainter extends CustomPainter {
|
|||
Color color,
|
||||
double leftPadding,
|
||||
double candleWidth,
|
||||
double Function(double) priceToY, {
|
||||
double Function(double) priceToY,
|
||||
double scrollOffset, {
|
||||
bool isDashed = false,
|
||||
}) {
|
||||
final paint = Paint()
|
||||
|
|
@ -278,7 +300,7 @@ class KlinePainter extends CustomPainter {
|
|||
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
if (data[i] != null) {
|
||||
final x = leftPadding + i * candleWidth + candleWidth / 2;
|
||||
final x = leftPadding + i * candleWidth + candleWidth / 2 - scrollOffset;
|
||||
final y = priceToY(data[i]!);
|
||||
|
||||
if (!started) {
|
||||
|
|
@ -322,12 +344,13 @@ class KlinePainter extends CustomPainter {
|
|||
double leftPadding,
|
||||
double candleWidth,
|
||||
double Function(double) priceToY,
|
||||
double scrollOffset,
|
||||
) {
|
||||
final paint = Paint()
|
||||
..color = _crossLineColor
|
||||
..strokeWidth = 0.5;
|
||||
|
||||
final x = leftPadding + index * candleWidth + candleWidth / 2;
|
||||
final x = leftPadding + index * candleWidth + candleWidth / 2 - scrollOffset;
|
||||
final kline = klines[index];
|
||||
final close = double.tryParse(kline.close) ?? 0;
|
||||
final y = priceToY(close);
|
||||
|
|
@ -452,6 +475,7 @@ class KlinePainter extends CustomPainter {
|
|||
oldDelegate.bollData != bollData ||
|
||||
oldDelegate.crossLineIndex != crossLineIndex ||
|
||||
oldDelegate.candleWidth != candleWidth ||
|
||||
oldDelegate.isDark != isDark;
|
||||
oldDelegate.isDark != isDark ||
|
||||
oldDelegate.scrollOffset != scrollOffset;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class KlineVolumePainter extends CustomPainter {
|
|||
final int crossLineIndex;
|
||||
final double? candleWidth;
|
||||
final bool isDark; // 深色模式
|
||||
final double scrollOffset; // 滚动偏移(像素)
|
||||
|
||||
static const Color _green = Color(0xFF10B981);
|
||||
static const Color _red = Color(0xFFEF4444);
|
||||
|
|
@ -23,6 +24,7 @@ class KlineVolumePainter extends CustomPainter {
|
|||
this.crossLineIndex = -1,
|
||||
this.candleWidth,
|
||||
this.isDark = false,
|
||||
this.scrollOffset = 0.0,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -82,6 +84,10 @@ class KlineVolumePainter extends CustomPainter {
|
|||
final actualCandleWidth = candleWidth ?? (chartWidth / klines.length);
|
||||
final barWidth = math.max(actualCandleWidth * 0.7, 2.0);
|
||||
|
||||
// 裁剪绘图区域
|
||||
canvas.save();
|
||||
canvas.clipRect(Rect.fromLTRB(leftPadding, 0, size.width - rightPadding, size.height));
|
||||
|
||||
for (int i = 0; i < klines.length; i++) {
|
||||
final kline = klines[i];
|
||||
final volume = double.tryParse(kline.volume) ?? 0;
|
||||
|
|
@ -91,7 +97,13 @@ class KlineVolumePainter extends CustomPainter {
|
|||
final isUp = close >= open;
|
||||
final color = isUp ? _green : _red;
|
||||
|
||||
final x = leftPadding + i * actualCandleWidth + actualCandleWidth / 2;
|
||||
final x = leftPadding + i * actualCandleWidth + actualCandleWidth / 2 - scrollOffset;
|
||||
|
||||
// 跳过不在可见区域的柱子
|
||||
if (x < leftPadding - actualCandleWidth || x > size.width - rightPadding + actualCandleWidth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final barHeight = (volume / maxVolume) * chartHeight;
|
||||
final y = size.height - bottomPadding - barHeight;
|
||||
|
||||
|
|
@ -104,9 +116,11 @@ class KlineVolumePainter extends CustomPainter {
|
|||
);
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
|
||||
// 绘制十字线
|
||||
if (crossLineIndex >= 0 && crossLineIndex < klines.length) {
|
||||
final x = leftPadding + crossLineIndex * actualCandleWidth + actualCandleWidth / 2;
|
||||
final x = leftPadding + crossLineIndex * actualCandleWidth + actualCandleWidth / 2 - scrollOffset;
|
||||
canvas.drawLine(
|
||||
Offset(x, 0),
|
||||
Offset(x, size.height),
|
||||
|
|
@ -141,6 +155,7 @@ class KlineVolumePainter extends CustomPainter {
|
|||
return oldDelegate.klines != klines ||
|
||||
oldDelegate.crossLineIndex != crossLineIndex ||
|
||||
oldDelegate.candleWidth != candleWidth ||
|
||||
oldDelegate.isDark != isDark;
|
||||
oldDelegate.isDark != isDark ||
|
||||
oldDelegate.scrollOffset != scrollOffset;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue