refactor(frontend): 优化K线图显示与交互体验

主要改动:
1. 隐藏技术指标 - 暂时隐藏MA/EMA/BOLL主图指标和MACD/KDJ/RSI副图指标
   - 保留全部代码,便于未来恢复(取消注释即可)
   - 调整高度分配:主图75%、成交量25%

2. 修复单指滑动(pan)问题
   - 移除错误的scale阈值检测 `(details.scale - 1.0).abs() > 0.01`
   - 改用 `pointerCount > 1` 区分单指滑动和双指缩放
   - 单指滑动现可正常左右拖动K线图

3. 优化首次加载显示
   - 新增 `_initialized` 标志控制初始化时机
   - 新增 `_initializeCandleWidth()` 方法动态计算K线宽度
   - K线首次加载时自动填满可视区域宽度
   - 数据量变化时自动重新初始化

技术细节:
- 使用 LayoutBuilder 获取实际图表宽度后再初始化
- 通过 postFrameCallback 确保在布局完成后执行初始化
- K线宽度限制在 3.0-30.0 像素范围内

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-19 06:31:24 -08:00
parent 7fb77bcc7e
commit 928d6c8df2
1 changed files with 75 additions and 54 deletions

View File

@ -50,6 +50,7 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
Offset? _startFocalPoint;
bool _isScaling = false;
double _chartWidth = 0.0; //
bool _initialized = false; // K线宽度
// K线宽度范围
static const double _minCandleWidth = 3.0;
@ -66,10 +67,24 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
@override
void initState() {
super.initState();
//
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToEnd();
});
// LayoutBuilder
}
/// K 线 K 线
void _initializeCandleWidth(double chartWidth) {
if (_initialized || widget.klines.isEmpty || chartWidth == 0) return;
_initialized = true;
_chartWidth = chartWidth;
// K 线
final idealWidth = chartWidth / widget.klines.length;
//
_candleWidth = idealWidth.clamp(_minCandleWidth, _maxCandleWidth);
_prevCandleWidth = _candleWidth;
//
_scrollToEnd();
}
void _scrollToEnd() {
@ -77,16 +92,15 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
//
final totalWidth = widget.klines.length * _candleWidth;
final maxScroll = math.max(0.0, totalWidth - _chartWidth);
setState(() {
_scrollX = maxScroll;
});
_scrollX = maxScroll;
}
@override
void didUpdateWidget(KlineChartWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// K 线
if (oldWidget.klines.length != widget.klines.length) {
_scrollToEnd();
_initialized = false;
}
}
@ -151,8 +165,8 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
body: SafeArea(
child: Column(
children: [
//
_buildIndicatorSelector(),
//
// _buildIndicatorSelector(),
// K线图区域
Expanded(child: _buildChartArea()),
//
@ -170,22 +184,23 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
//
Row(
children: [
_buildIndicatorChip('MA', _selectedMainIndicator == 0, () {
setState(() => _selectedMainIndicator = 0);
}),
const SizedBox(width: 8),
_buildIndicatorChip('MACD', _selectedSubIndicator == 0, () {
setState(() => _selectedSubIndicator = 0);
}),
const SizedBox(width: 8),
_buildIndicatorChip('KDJ', _selectedSubIndicator == 1, () {
setState(() => _selectedSubIndicator = 1);
}),
],
),
//
// Row(
// children: [
// _buildIndicatorChip('MA', _selectedMainIndicator == 0, () {
// setState(() => _selectedMainIndicator = 0);
// }),
// const SizedBox(width: 8),
// _buildIndicatorChip('MACD', _selectedSubIndicator == 0, () {
// setState(() => _selectedSubIndicator = 0);
// }),
// const SizedBox(width: 8),
// _buildIndicatorChip('KDJ', _selectedSubIndicator == 1, () {
// setState(() => _selectedSubIndicator = 1);
// }),
// ],
// ),
const SizedBox(), //
//
IconButton(
icon: Icon(
@ -288,6 +303,14 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
//
_chartWidth = chartWidth;
// K 线 K 线
if (!_initialized && widget.klines.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializeCandleWidth(chartWidth);
if (mounted) setState(() {});
});
}
if (widget.klines.isEmpty) {
return Center(
child: Column(
@ -313,16 +336,16 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
final bollData = processor.calculateBOLL();
final emaData = processor.calculateEMA([5, 10, 20]);
// -
// -
// 75%25%
final mainChartHeight = widget.showVolume
? (widget.isFullScreen ? chartHeight * 0.65 : chartHeight * 0.6)
: (widget.isFullScreen ? chartHeight * 0.75 : chartHeight * 0.7);
? chartHeight * 0.75
: chartHeight * 1.0;
final volumeHeight = widget.showVolume
? (widget.isFullScreen ? chartHeight * 0.12 : chartHeight * 0.2)
? chartHeight * 0.25
: 0.0;
final indicatorHeight = widget.isFullScreen
? (widget.showVolume ? chartHeight * 0.23 : chartHeight * 0.25)
: (widget.showVolume ? chartHeight * 0.2 : chartHeight * 0.3);
//
// final indicatorHeight = chartHeight * 0.20;
return Column(
children: [
@ -382,21 +405,21 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
),
),
),
// MACD/KDJ/RSI
SizedBox(
height: indicatorHeight,
child: CustomPaint(
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,
),
),
),
// MACD/KDJ/RSI-
// SizedBox(
// height: indicatorHeight,
// child: CustomPaint(
// 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,
// ),
// ),
// ),
],
);
},
@ -453,8 +476,8 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
void _onScaleUpdate(ScaleUpdateDetails details) {
setState(() {
// K线宽度
if (_isScaling || (details.scale - 1.0).abs() > 0.01) {
_isScaling = true;
//
if (_isScaling) {
final newCandleWidth = (_prevCandleWidth * details.scale).clamp(_minCandleWidth, _maxCandleWidth);
// K线位置不变
@ -469,10 +492,8 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
}
_candleWidth = newCandleWidth;
}
// - 使
if (!_isScaling && _startFocalPoint != null) {
} 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);