From 5d22e342886f586b38c3309f62f2f1e537b6c5f1 Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 19 Jan 2026 21:04:48 -0800 Subject: [PATCH] =?UTF-8?q?feat(kline):=20=E6=94=AF=E6=8C=81=E5=B7=A6?= =?UTF-8?q?=E5=8F=B3=E5=B9=B3=E7=A7=BB=E6=9F=A5=E7=9C=8B=E5=8E=86=E5=8F=B2?= =?UTF-8?q?K=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 初始状态:最新K线在屏幕中心 - 向左滑动:查看更早的历史K线 - 向右滑动:回到最新K线 - 平移后可显示整屏K线数据 Co-Authored-By: Claude Opus 4.5 --- .../kline_chart/kline_chart_widget.dart | 105 ++++++++---------- 1 file changed, 45 insertions(+), 60 deletions(-) diff --git a/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_chart_widget.dart b/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_chart_widget.dart index 0cd5b451..6ec0eb4e 100644 --- a/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_chart_widget.dart +++ b/frontend/mining-app/lib/presentation/widgets/kline_chart/kline_chart_widget.dart @@ -64,55 +64,46 @@ class _KlineChartWidgetState extends State { 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 { ); } - // 手势处理 - 大厂标准做法:基于像素的滚动和缩放 + // 手势处理 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 { 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 { 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 { ); } - // 数据量足够时,只取最近的 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, ); }