feat(kline): 支持左右平移查看历史K线

- 初始状态:最新K线在屏幕中心
- 向左滑动:查看更早的历史K线
- 向右滑动:回到最新K线
- 平移后可显示整屏K线数据

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-19 21:04:48 -08:00
parent f149c2a06a
commit 5d22e34288
1 changed files with 45 additions and 60 deletions

View File

@ -64,55 +64,46 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
bool _showCrossLine = false; bool _showCrossLine = false;
int _crossLineIndex = -1; int _crossLineIndex = -1;
//
int _displayStartIndex = 0;
int _startDisplayIndex = 0; //
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// LayoutBuilder // LayoutBuilder
} }
/// K 线 ///
/// ///
/// ///
/// - K线从左边开始排列 /// - K线从左边开始排列
/// - /// -
/// - K线在屏幕中心 /// - K线在屏幕中心
void _initializeCandleWidth(double chartWidth) { void _initializeCandleWidth(double chartWidth) {
if (_initialized || widget.klines.isEmpty || chartWidth == 0) return; if (_initialized || widget.klines.isEmpty || chartWidth == 0) return;
_initialized = true; _initialized = true;
_chartWidth = chartWidth; _chartWidth = chartWidth;
// K线宽度
// _candleWidth 8.0
_prevCandleWidth = _candleWidth; _prevCandleWidth = _candleWidth;
// K线在屏幕中心 // K线在屏幕中心
_scrollToCenter(); _initDisplayStartIndex();
} }
/// 使 K 线 /// K线在屏幕中心
/// void _initDisplayStartIndex() {
///
/// - K线从左边开始排列
/// - <=
/// - K线在屏幕中心
void _scrollToCenter() {
if (widget.klines.isEmpty || _chartWidth == 0) return; if (widget.klines.isEmpty || _chartWidth == 0) return;
final totalWidth = widget.klines.length * _candleWidth; // K线
final int halfScreenCount = (_chartWidth / 2 / _candleWidth).ceil();
if (totalWidth <= _chartWidth) { if (widget.klines.length <= halfScreenCount) {
// //
_scrollX = 0; _displayStartIndex = 0;
} else { } else {
// K线在屏幕中心的滚动位置 // K线在屏幕中心
// K线的中心位置 = (K线数量 - 0.5) * K线宽度 _displayStartIndex = widget.klines.length - halfScreenCount;
final lastKlineCenter = (widget.klines.length - 0.5) * _candleWidth;
// = K线中心 -
final targetScroll = lastKlineCenter - _chartWidth / 2;
//
final maxScroll = totalWidth - _chartWidth;
_scrollX = targetScroll.clamp(0.0, maxScroll);
} }
} }
@ -496,10 +487,10 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
); );
} }
// - //
void _onScaleStart(ScaleStartDetails details) { void _onScaleStart(ScaleStartDetails details) {
_prevCandleWidth = _candleWidth; _prevCandleWidth = _candleWidth;
_startScrollX = _scrollX; _startDisplayIndex = _displayStartIndex;
_startFocalPoint = details.focalPoint; _startFocalPoint = details.focalPoint;
// pointerCount > 1 // pointerCount > 1
_isScaling = details.pointerCount > 1; _isScaling = details.pointerCount > 1;
@ -507,29 +498,24 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
void _onScaleUpdate(ScaleUpdateDetails details) { void _onScaleUpdate(ScaleUpdateDetails details) {
setState(() { setState(() {
// K线宽度 // K线宽度
//
if (_isScaling) { if (_isScaling) {
final newCandleWidth = (_prevCandleWidth * details.scale).clamp(_minCandleWidth, _maxCandleWidth); 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; _candleWidth = newCandleWidth;
// K线在中心
_initDisplayStartIndex();
} else if (_startFocalPoint != null) { } else if (_startFocalPoint != null) {
// - 使 // -
final dx = details.focalPoint.dx - _startFocalPoint!.dx; final dx = details.focalPoint.dx - _startFocalPoint!.dx;
final totalWidth = widget.klines.length * _candleWidth; // K线根数
final maxScroll = math.max(0.0, totalWidth - _chartWidth); final indexDelta = (dx / _candleWidth).round();
_scrollX = (_startScrollX - dx).clamp(0.0, maxScroll);
// 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(() { setState(() {
_showCrossLine = true; _showCrossLine = true;
// K线索引 // K线索引
final absoluteX = _scrollX + position.dx; final localIndex = (position.dx / _candleWidth).floor();
final index = (absoluteX / _candleWidth).floor().clamp(0, widget.klines.length - 1); final index = (_displayStartIndex + localIndex).clamp(0, widget.klines.length - 1);
_crossLineIndex = index; _crossLineIndex = index;
}); });
} }
// - K线数据 // - _displayStartIndex K线数据
// K线在屏幕中心
_VisibleData _getVisibleData(double chartWidth) { _VisibleData _getVisibleData(double chartWidth) {
if (widget.klines.isEmpty || chartWidth == 0) { if (widget.klines.isEmpty || chartWidth == 0) {
return _VisibleData(klines: [], startIndex: 0, candleWidth: _candleWidth); return _VisibleData(klines: [], startIndex: 0, candleWidth: _candleWidth);
} }
// K线 // K线
final int halfScreenCount = (chartWidth / 2 / _candleWidth).ceil(); final int fullScreenCount = (chartWidth / _candleWidth).ceil() + 1;
// //
if (widget.klines.length <= halfScreenCount) { if (widget.klines.length <= fullScreenCount) {
return _VisibleData( return _VisibleData(
klines: widget.klines, klines: widget.klines,
startIndex: 0, startIndex: 0,
@ -587,11 +572,11 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
); );
} }
// halfScreenCount // _displayStartIndex
final int startIndex = widget.klines.length - halfScreenCount; final int endIndex = math.min(_displayStartIndex + fullScreenCount, widget.klines.length);
return _VisibleData( return _VisibleData(
klines: widget.klines.sublist(startIndex), klines: widget.klines.sublist(_displayStartIndex, endIndex),
startIndex: startIndex, startIndex: _displayStartIndex,
candleWidth: _candleWidth, candleWidth: _candleWidth,
); );
} }