feat(kline): 支持左右平移查看历史K线
- 初始状态:最新K线在屏幕中心 - 向左滑动:查看更早的历史K线 - 向右滑动:回到最新K线 - 平移后可显示整屏K线数据 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f149c2a06a
commit
5d22e34288
|
|
@ -64,55 +64,46 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
bool _showCrossLine = false;
|
||||
int _crossLineIndex = -1;
|
||||
|
||||
// 显示起始索引(用于平移控制)
|
||||
int _displayStartIndex = 0;
|
||||
int _startDisplayIndex = 0; // 平移开始时的索引
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 初始化会在 LayoutBuilder 中完成
|
||||
}
|
||||
|
||||
/// 初始化 K 线宽度和滚动位置
|
||||
/// 初始化显示位置
|
||||
///
|
||||
/// 逻辑:
|
||||
/// - K线从左边开始排列
|
||||
/// - 数据不够时,保持默认宽度,有多少显示多少
|
||||
/// - 数据量大时,计算滚动位置让最新K线在屏幕中心
|
||||
/// - 数据不够时,显示全部
|
||||
/// - 数据量大时,计算起始索引让最新K线在屏幕中心
|
||||
void _initializeCandleWidth(double chartWidth) {
|
||||
if (_initialized || widget.klines.isEmpty || chartWidth == 0) return;
|
||||
|
||||
_initialized = true;
|
||||
_chartWidth = chartWidth;
|
||||
|
||||
// 保持默认K线宽度,不根据数据量缩放
|
||||
// _candleWidth 保持初始值 8.0
|
||||
_prevCandleWidth = _candleWidth;
|
||||
|
||||
// 计算滚动位置,让最新K线在屏幕中心
|
||||
_scrollToCenter();
|
||||
// 计算起始索引,让最新K线在屏幕中心
|
||||
_initDisplayStartIndex();
|
||||
}
|
||||
|
||||
/// 滚动使最新 K 线在屏幕中心显示
|
||||
///
|
||||
/// 计算逻辑:
|
||||
/// - K线从左边开始排列
|
||||
/// - 数据不够时(总宽度 <= 屏幕宽度),不滚动,有多少显示多少
|
||||
/// - 数据量大时,计算滚动位置让最新K线在屏幕中心
|
||||
void _scrollToCenter() {
|
||||
/// 初始化显示起始索引,让最新K线在屏幕中心
|
||||
void _initDisplayStartIndex() {
|
||||
if (widget.klines.isEmpty || _chartWidth == 0) return;
|
||||
|
||||
final totalWidth = widget.klines.length * _candleWidth;
|
||||
// 计算屏幕中心到左边缘能显示多少根K线
|
||||
final int halfScreenCount = (_chartWidth / 2 / _candleWidth).ceil();
|
||||
|
||||
if (totalWidth <= _chartWidth) {
|
||||
// 数据不够,不滚动,从左开始显示
|
||||
_scrollX = 0;
|
||||
if (widget.klines.length <= halfScreenCount) {
|
||||
// 数据不够半屏,从头开始显示
|
||||
_displayStartIndex = 0;
|
||||
} else {
|
||||
// 数据量大,计算让最新K线在屏幕中心的滚动位置
|
||||
// 最新K线的中心位置 = (K线数量 - 0.5) * 单根K线宽度
|
||||
final lastKlineCenter = (widget.klines.length - 0.5) * _candleWidth;
|
||||
// 目标滚动位置 = 最新K线中心 - 屏幕宽度的一半
|
||||
final targetScroll = lastKlineCenter - _chartWidth / 2;
|
||||
// 限制在有效滚动范围内
|
||||
final maxScroll = totalWidth - _chartWidth;
|
||||
_scrollX = targetScroll.clamp(0.0, maxScroll);
|
||||
// 数据量大,设置起始索引让最新K线在屏幕中心
|
||||
_displayStartIndex = widget.klines.length - halfScreenCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -496,10 +487,10 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
// 手势处理 - 大厂标准做法:基于像素的滚动和缩放
|
||||
// 手势处理
|
||||
void _onScaleStart(ScaleStartDetails details) {
|
||||
_prevCandleWidth = _candleWidth;
|
||||
_startScrollX = _scrollX;
|
||||
_startDisplayIndex = _displayStartIndex;
|
||||
_startFocalPoint = details.focalPoint;
|
||||
// 检测是否为两指缩放(pointerCount > 1表示多指触摸)
|
||||
_isScaling = details.pointerCount > 1;
|
||||
|
|
@ -507,29 +498,24 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
|
||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
setState(() {
|
||||
// 两指缩放:调整K线宽度,围绕焦点缩放
|
||||
// 只有在明确是缩放模式(多指触摸)时才进行缩放
|
||||
// 两指缩放:调整K线宽度
|
||||
if (_isScaling) {
|
||||
final newCandleWidth = (_prevCandleWidth * details.scale).clamp(_minCandleWidth, _maxCandleWidth);
|
||||
|
||||
// 围绕焦点缩放:调整滚动位置以保持焦点处的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;
|
||||
// 缩放后重新计算起始索引,保持最新K线在中心
|
||||
_initDisplayStartIndex();
|
||||
} else if (_startFocalPoint != null) {
|
||||
// 单指平移 - 直接使用像素位移
|
||||
// 单指平移 - 根据像素位移计算索引变化
|
||||
final dx = details.focalPoint.dx - _startFocalPoint!.dx;
|
||||
final totalWidth = widget.klines.length * _candleWidth;
|
||||
final maxScroll = math.max(0.0, totalWidth - _chartWidth);
|
||||
_scrollX = (_startScrollX - dx).clamp(0.0, maxScroll);
|
||||
// 滑动的像素转换为K线根数
|
||||
final indexDelta = (dx / _candleWidth).round();
|
||||
|
||||
// 计算屏幕中心到左边缘能显示多少根K线(最大起始索引)
|
||||
final int halfScreenCount = (_chartWidth / 2 / _candleWidth).ceil();
|
||||
final int maxStartIndex = math.max(0, widget.klines.length - halfScreenCount);
|
||||
|
||||
// 更新起始索引,限制范围:0 到 maxStartIndex
|
||||
_displayStartIndex = (_startDisplayIndex - indexDelta).clamp(0, maxStartIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -561,25 +547,24 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
setState(() {
|
||||
_showCrossLine = true;
|
||||
|
||||
// 计算对应的K线索引(基于像素位置)
|
||||
final absoluteX = _scrollX + position.dx;
|
||||
final index = (absoluteX / _candleWidth).floor().clamp(0, widget.klines.length - 1);
|
||||
// 计算对应的K线索引(基于像素位置和起始索引)
|
||||
final localIndex = (position.dx / _candleWidth).floor();
|
||||
final index = (_displayStartIndex + localIndex).clamp(0, widget.klines.length - 1);
|
||||
_crossLineIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
// 数据处理 - 计算要显示的K线数据
|
||||
// 逻辑:最新K线在屏幕中心,只取中心到左边缘能显示的数量
|
||||
// 数据处理 - 根据 _displayStartIndex 计算要显示的K线数据
|
||||
_VisibleData _getVisibleData(double chartWidth) {
|
||||
if (widget.klines.isEmpty || chartWidth == 0) {
|
||||
return _VisibleData(klines: [], startIndex: 0, candleWidth: _candleWidth);
|
||||
}
|
||||
|
||||
// 计算屏幕中心到左边缘能显示多少根K线
|
||||
final int halfScreenCount = (chartWidth / 2 / _candleWidth).ceil();
|
||||
// 计算整屏能显示多少根K线
|
||||
final int fullScreenCount = (chartWidth / _candleWidth).ceil() + 1;
|
||||
|
||||
// 数据量不够半屏时,显示全部
|
||||
if (widget.klines.length <= halfScreenCount) {
|
||||
// 数据量不够整屏时,显示全部
|
||||
if (widget.klines.length <= fullScreenCount) {
|
||||
return _VisibleData(
|
||||
klines: widget.klines,
|
||||
startIndex: 0,
|
||||
|
|
@ -587,11 +572,11 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
// 数据量足够时,只取最近的 halfScreenCount 根,让最新那根在屏幕中心
|
||||
final int startIndex = widget.klines.length - halfScreenCount;
|
||||
// 根据 _displayStartIndex 取数据
|
||||
final int endIndex = math.min(_displayStartIndex + fullScreenCount, widget.klines.length);
|
||||
return _VisibleData(
|
||||
klines: widget.klines.sublist(startIndex),
|
||||
startIndex: startIndex,
|
||||
klines: widget.klines.sublist(_displayStartIndex, endIndex),
|
||||
startIndex: _displayStartIndex,
|
||||
candleWidth: _candleWidth,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue