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