You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter实现iOS风格折线图的技术问题求助

iOS风格Flutter折线图实现方案

需求与现存瓶颈

我需要为Flutter应用实现标准iOS风格折线图,目前已完成基础效果,但遇到三个核心问题:

  • 数据末端的灰色竖线需被灰色圆形完全覆盖
  • 折线要同时具备阶梯感与平滑曲线效果
  • X轴需显示00:00和23:30标签,且中间位置带有灰色标记点

解决方案与完整代码

以下是针对上述问题优化后的实现代码:

import 'package:flutter/cupertino.dart';
import 'package:fl_chart/fl_chart.dart';

class IOSStyleLineChart extends StatelessWidget {
  const IOSStyleLineChart({super.key});

  @override
  Widget build(BuildContext context) {
    // 原始数据(固定时间点便于演示)
    List<Map<String, dynamic>> rawData = [
      {"time": DateTime(2024, 1, 1, 0, 0), "xp": 0},
      {"time": DateTime(2024, 1, 1, 4, 0), "xp": 10},
      {"time": DateTime(2024, 1, 1, 3, 30), "xp": 24},
      {"time": DateTime(2024, 1, 1, 2, 15), "xp": 400},
      {"time": DateTime(2024, 1, 1, 2, 1), "xp": 500},
      {"time": DateTime(2024, 1, 1, 1, 45), "xp": 700},
      {"time": DateTime(2024, 1, 1, 1, 0), "xp": 750},
      {"time": DateTime(2024, 1, 1, 23, 30), "xp": 800},
    ];

    // 处理数据:拆分点以支持阶梯+曲线混合效果
    List<FlSpot> processedSpots = [];
    for (int i = 0; i < rawData.length; i++) {
      final current = rawData[i];
      final currentTime = current["time"] as DateTime;
      final currentXp = current["xp"] as int;
      double currentX = currentTime.hour + currentTime.minute / 60;

      processedSpots.add(FlSpot(currentX, currentXp.toDouble()));

      // 为阶梯效果添加水平过渡点(最后一个点除外)
      if (i < rawData.length - 1) {
        final next = rawData[i + 1];
        final nextTime = next["time"] as DateTime;
        double nextX = nextTime.hour + nextTime.minute / 60;
        processedSpots.add(FlSpot(nextX, currentXp.toDouble()));
      }
    }

    return LineChart(
      LineChartData(
        minX: 0,
        maxX: 24,
        minY: 0,
        maxY: 900,
        gridData: FlGridData(
          show: true,
          drawVerticalGrid: true,
          verticalInterval: 12, // 中间标记点对应X轴12点位置
          getDrawingVerticalGridLine: (value) {
            return FlLine(
              color: CupertinoColors.systemGrey4,
              strokeWidth: value == 12 ? 2 : 0, // 仅中间位置显示灰色标记
            );
          },
          drawHorizontalGrid: false,
        ),
        titlesData: FlTitlesData(
          show: true,
          bottomTitles: AxisTitles(
            sideTitles: SideTitles(
              showTitles: true,
              getTitlesWidget: (value, meta) {
                String title = "";
                if (value == 0) title = "00:00";
                else if (value == 23.5) title = "23:30";
                return Text(
                  title,
                  style: TextStyle(color: CupertinoColors.systemGrey),
                );
              },
              reservedSize: 20,
            ),
          ),
          leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
          topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
          rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
        ),
        borderData: FlBorderData(show: false),
        lineBarsData: [
          // 平滑曲线层:实现曲线效果
          LineChartBarData(
            spots: processedSpots,
            barWidth: 2,
            isCurved: true,
            curveSmoothness: 0.3,
            dotData: const FlDotData(show: false),
            belowBarData: const BarAreaData(show: false),
            color: CupertinoColors.destructiveRed.withOpacity(0.7),
          ),
          // 阶梯线层:实现阶梯感,叠加在曲线上
          LineChartBarData(
            spots: processedSpots,
            barWidth: 2,
            isStepLineChart: true,
            dotData: const FlDotData(show: false),
            belowBarData: const BarAreaData(show: false),
            color: CupertinoColors.destructiveRed,
          ),
          // 末端灰色竖线+覆盖圆
          LineChartBarData(
            spots: [FlSpot(23.5, 0), FlSpot(23.5, 800)],
            barWidth: 1,
            color: CupertinoColors.systemGrey4,
            dotData: FlDotData(
              show: true,
              getDotPainter: (spot, percent, barData, index) {
                if (index == 1) { // 仅在竖线顶部显示覆盖圆
                  return FlDotCirclePainter(
                    radius: 6,
                    color: CupertinoColors.systemGrey4,
                    strokeWidth: 2,
                    strokeColor: CupertinoColors.white,
                  );
                }
                return const FlDotCirclePainter(radius: 0);
              },
            ),
          ),
        ],
      ),
    );
  }
}

核心优化说明

  1. 末端竖线覆盖:新增独立折线层绘制灰色竖线,同时在竖线顶部通过自定义FlDotPainter绘制带白边的灰色圆形,实现覆盖效果。
  2. 阶梯+曲线混合:将原始数据拆分为阶梯拐点的过渡点,叠加两个折线层——一个开启isCurved实现平滑曲线,另一个开启isStepLineChart实现阶梯感,通过透明度区分层级。
  3. X轴配置:通过bottomTitles自定义显示00:00和23:30标签;利用FlGridData在X轴12点位置绘制灰色标记点。

内容的提问来源于stack exchange,提问作者EwanCharalambides

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 21:33:19