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:
parent
7fb77bcc7e
commit
928d6c8df2
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue