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

- 初始状态:最新K线在屏幕中心(保持原有逻辑不变)
- 用户平移后可查看全部历史数据
- 切换周期时重置平移状态

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-19 21:25:59 -08:00
parent f149c2a06a
commit 684367941d
1 changed files with 43 additions and 18 deletions

View File

@ -64,6 +64,10 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
bool _showCrossLine = false; bool _showCrossLine = false;
int _crossLineIndex = -1; int _crossLineIndex = -1;
//
bool _userHasPanned = false;
int _panStartIndex = 0; //
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -130,6 +134,8 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
// K 线 // K 线
if (oldWidget.klines.length != widget.klines.length) { if (oldWidget.klines.length != widget.klines.length) {
_initialized = false; _initialized = false;
_userHasPanned = false; //
_scrollX = 0;
} }
} }
@ -496,23 +502,27 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
); );
} }
// - //
void _onScaleStart(ScaleStartDetails details) { void _onScaleStart(ScaleStartDetails details) {
_prevCandleWidth = _candleWidth; _prevCandleWidth = _candleWidth;
_startScrollX = _scrollX;
_startFocalPoint = details.focalPoint; _startFocalPoint = details.focalPoint;
// pointerCount > 1
_isScaling = details.pointerCount > 1; _isScaling = details.pointerCount > 1;
// _scrollX
if (!_userHasPanned && !_isScaling) {
final int halfScreenCount = (_chartWidth / 2 / _candleWidth).ceil();
final int startIndex = math.max(0, widget.klines.length - halfScreenCount);
_scrollX = startIndex * _candleWidth;
}
_startScrollX = _scrollX;
} }
void _onScaleUpdate(ScaleUpdateDetails details) { void _onScaleUpdate(ScaleUpdateDetails details) {
setState(() { setState(() {
// 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) { if (_startFocalPoint != null && _chartWidth > 0) {
final focalRatio = _startFocalPoint!.dx / _chartWidth; final focalRatio = _startFocalPoint!.dx / _chartWidth;
final oldFocalIndex = (_startScrollX + _startFocalPoint!.dx) / _prevCandleWidth; final oldFocalIndex = (_startScrollX + _startFocalPoint!.dx) / _prevCandleWidth;
@ -524,12 +534,15 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
} }
_candleWidth = newCandleWidth; _candleWidth = newCandleWidth;
_userHasPanned = true; //
} 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 int halfScreenCount = (_chartWidth / 2 / _candleWidth).ceil();
final double maxScroll = math.max(0.0, (widget.klines.length - halfScreenCount) * _candleWidth);
_scrollX = (_startScrollX - dx).clamp(0.0, maxScroll); _scrollX = (_startScrollX - dx).clamp(0.0, maxScroll);
_userHasPanned = true;
} }
}); });
} }
@ -569,7 +582,6 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
} }
// - K线数据 // - 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);
@ -578,7 +590,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
// K线 // K线
final int halfScreenCount = (chartWidth / 2 / _candleWidth).ceil(); final int halfScreenCount = (chartWidth / 2 / _candleWidth).ceil();
// //
if (widget.klines.length <= halfScreenCount) { if (widget.klines.length <= halfScreenCount) {
return _VisibleData( return _VisibleData(
klines: widget.klines, klines: widget.klines,
@ -587,13 +599,26 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
); );
} }
// halfScreenCount //
final int startIndex = widget.klines.length - halfScreenCount; if (_userHasPanned) {
return _VisibleData( // _scrollX
klines: widget.klines.sublist(startIndex), final int fullScreenCount = (chartWidth / _candleWidth).ceil() + 1;
startIndex: startIndex, final int startIndex = (_scrollX / _candleWidth).floor().clamp(0, widget.klines.length - 1);
candleWidth: _candleWidth, final int endIndex = math.min(startIndex + fullScreenCount, widget.klines.length);
); return _VisibleData(
klines: widget.klines.sublist(startIndex, endIndex),
startIndex: startIndex,
candleWidth: _candleWidth,
);
} else {
// halfScreenCount
final int startIndex = widget.klines.length - halfScreenCount;
return _VisibleData(
klines: widget.klines.sublist(startIndex),
startIndex: startIndex,
candleWidth: _candleWidth,
);
}
} }
int _getVisibleCount() { int _getVisibleCount() {