在这里插入图片描述

前言

数据图表是打卡工具类应用中帮助用户分析打卡习惯的重要功能。通过可视化的图表展示,用户可以直观地了解自己的打卡趋势、周期规律和长期变化。本文将详细介绍如何在Flutter和OpenHarmony平台上实现功能丰富的打卡数据图表组件,包括柱状图、折线图等常见图表类型。

数据图表的设计需要在信息准确性和视觉美观性之间取得平衡。图表应该能够准确反映数据,同时通过合理的颜色、动画和交互设计,让用户愿意查看和分析自己的打卡数据。我们将实现周视图和月视图两种展示方式,满足不同的分析需求。

Flutter柱状图实现

首先定义图表数据模型:

class ChartData {
  final String label;
  final double value;
  final bool isHighlighted;

  const ChartData({
    required this.label,
    required this.value,
    this.isHighlighted = false,
  });
}

class WeeklyChartWidget extends StatefulWidget {
  final List<ChartData> data;
  final double maxValue;

  const WeeklyChartWidget({
    Key? key,
    required this.data,
    this.maxValue = 1.0,
  }) : super(key: key);

  
  State<WeeklyChartWidget> createState() => _WeeklyChartWidgetState();
}

ChartData模型包含标签、数值和高亮标识三个属性。label用于显示X轴标签(如星期几),value是0到1之间的归一化数值,isHighlighted用于标识当天或特殊日期。WeeklyChartWidget接收数据列表和最大值参数,maxValue用于计算柱状图的高度比例。

实现柱状图动画:

class _WeeklyChartWidgetState extends State<WeeklyChartWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    );
    _animation = CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic);
    _controller.forward();
  }

  
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) => _buildChart(),
    );
  }
}

柱状图使用动画让数据"生长"出来,从0逐渐增长到目标高度。AnimationController控制动画时长为800毫秒,easeOutCubic缓动曲线让动画先快后慢,在接近目标值时减速,视觉效果更加自然。AnimatedBuilder在动画值变化时重建图表,实现平滑的动画效果。

构建柱状图布局:

Widget _buildChart() {
  return Container(
    height: 200,
    padding: const EdgeInsets.all(16),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      crossAxisAlignment: CrossAxisAlignment.end,
      children: widget.data.map((item) => _buildBar(item)).toList(),
    ),
  );
}

Widget _buildBar(ChartData item) {
  final barHeight = 150 * (item.value / widget.maxValue) * _animation.value;
  return Column(
    mainAxisAlignment: MainAxisAlignment.end,
    children: [
      Container(
        width: 32,
        height: barHeight,
        decoration: BoxDecoration(
          color: item.isHighlighted ? Colors.orange : Colors.blue.shade400,
          borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
        ),
      ),
      const SizedBox(height: 8),
      Text(item.label, style: const TextStyle(fontSize: 12)),
    ],
  );
}

图表使用Row布局水平排列所有柱状图,spaceEvenly让柱子均匀分布。每个柱子的高度通过数据值、最大值和动画进度计算得出。高亮的柱子使用橙色,普通柱子使用蓝色,这种颜色区分帮助用户快速定位当天的数据。柱子顶部使用圆角,底部与X轴对齐,视觉效果更加美观。

OpenHarmony柱状图实现

在鸿蒙系统中定义图表组件:

interface ChartData {
  label: string
  value: number
  isHighlighted: boolean
}

@Component
struct WeeklyChartWidget {
  @Prop data: ChartData[] = []
  @Prop maxValue: number = 1.0
  @State animationProgress: number = 0
}

鸿蒙的图表组件使用相同的数据结构。@State装饰的animationProgress用于控制动画进度,从0逐渐变化到1,驱动柱状图的高度变化。这种状态驱动的动画方式与Flutter的AnimationController有异曲同工之妙。

启动图表动画:

aboutToAppear() {
  animateTo({
    duration: 800,
    curve: Curve.EaseOut
  }, () => {
    this.animationProgress = 1
  })
}

aboutToAppear生命周期方法在组件即将显示时调用,我们在这里启动动画。animateTo函数将animationProgress从0变化到1,duration设为800毫秒与Flutter保持一致。当animationProgress变化时,由于它是@State状态,会自动触发UI更新,柱状图会平滑地"生长"出来。

构建柱状图:

build() {
  Column() {
    Row() {
      ForEach(this.data, (item: ChartData) => {
        this.BarItem(item)
      })
    }
    .width('100%')
    .height(200)
    .justifyContent(FlexAlign.SpaceEvenly)
    .alignItems(VerticalAlign.Bottom)
  }
  .padding(16)
}

图表容器使用Column包裹,内部Row实现水平布局。justifyContent设为SpaceEvenly让柱子均匀分布,alignItems设为Bottom让所有柱子底部对齐。ForEach遍历数据数组,为每个数据点生成一个柱子。这种布局方式简洁高效,代码可读性强。

实现单个柱子:

@Builder
BarItem(item: ChartData) {
  Column() {
    Column()
      .width(32)
      .height(150 * (item.value / this.maxValue) * this.animationProgress)
      .backgroundColor(item.isHighlighted ? '#FF9500' : '#42A5F5')
      .borderRadius({ topLeft: 4, topRight: 4 })
    
    Text(item.label)
      .fontSize(12)
      .fontColor('#666666')
      .margin({ top: 8 })
  }
  .justifyContent(FlexAlign.End)
}

每个柱子由柱体和标签组成。柱体高度通过数据值、最大值和动画进度三者相乘计算得出,这样在动画过程中柱子会从0逐渐增长到目标高度。borderRadius只设置顶部圆角,底部保持直角与X轴对齐。justifyContent设为End让内容底部对齐,确保所有柱子的底部在同一水平线上。

折线图实现

Flutter中实现折线图:

class LineChartWidget extends StatelessWidget {
  final List<ChartData> data;

  const LineChartWidget({Key? key, required this.data}) : super(key: key);

  
  Widget build(BuildContext context) {
    return CustomPaint(
      size: const Size(double.infinity, 200),
      painter: LineChartPainter(data: data),
    );
  }
}

class LineChartPainter extends CustomPainter {
  final List<ChartData> data;

  LineChartPainter({required this.data});

  
  void paint(Canvas canvas, Size size) {
    if (data.isEmpty) return;
    
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke;
    
    final path = Path();
    final stepX = size.width / (data.length - 1);
    
    for (int i = 0; i < data.length; i++) {
      final x = i * stepX;
      final y = size.height * (1 - data[i].value);
      if (i == 0) {
        path.moveTo(x, y);
      } else {
        path.lineTo(x, y);
      }
    }
    
    canvas.drawPath(path, paint);
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

折线图使用CustomPaint和CustomPainter实现自定义绑制。paint方法遍历数据点,计算每个点的X和Y坐标,然后使用Path连接所有点形成折线。X坐标根据数据点索引和步长计算,Y坐标根据数据值和画布高度计算(注意Y轴是反向的,所以用1减去数据值)。这种实现方式灵活且高效,可以轻松扩展为曲线图或面积图。

添加数据点标记:

void _drawDataPoints(Canvas canvas, Size size) {
  final pointPaint = Paint()
    ..color = Colors.blue
    ..style = PaintingStyle.fill;
  
  final stepX = size.width / (data.length - 1);
  
  for (int i = 0; i < data.length; i++) {
    final x = i * stepX;
    final y = size.height * (1 - data[i].value);
    canvas.drawCircle(Offset(x, y), 4, pointPaint);
  }
}

在折线的每个数据点位置绘制小圆点,让用户能够清楚地看到具体的数据位置。圆点半径设为4像素,既能清晰可见又不会过于突兀。这种数据点标记在折线图中非常常见,能够帮助用户准确读取数据值。

OpenHarmony折线图

鸿蒙中使用Canvas绘制折线图:

@Component
struct LineChartWidget {
  @Prop data: ChartData[] = []
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  
  build() {
    Canvas(this.context)
      .width('100%')
      .height(200)
      .onReady(() => this.drawChart())
  }
}

鸿蒙使用Canvas组件进行自定义绑制,需要创建RenderingContextSettings和CanvasRenderingContext2D对象。onReady回调在Canvas准备就绪后触发,我们在这里调用绑制方法。这种API设计与Web的Canvas API非常相似,对于有Web开发经验的开发者来说很容易上手。

实现折线绑制:

drawChart() {
  const width = this.context.width
  const height = this.context.height
  const stepX = width / (this.data.length - 1)
  
  this.context.beginPath()
  this.context.strokeStyle = '#2196F3'
  this.context.lineWidth = 2
  
  this.data.forEach((item, index) => {
    const x = index * stepX
    const y = height * (1 - item.value)
    if (index === 0) {
      this.context.moveTo(x, y)
    } else {
      this.context.lineTo(x, y)
    }
  })
  
  this.context.stroke()
}

折线绘制逻辑与Flutter版本类似,遍历数据点计算坐标并连接成线。beginPath开始新路径,moveTo移动到起始点,lineTo连接后续点,最后stroke绑制路径。strokeStyle设置线条颜色,lineWidth设置线条宽度。这种命令式的绘制API虽然比声明式UI稍显繁琐,但提供了更大的灵活性。

图表交互功能

添加触摸查看数据详情:

class InteractiveChart extends StatefulWidget {
  final List<ChartData> data;

  
  State<InteractiveChart> createState() => _InteractiveChartState();
}

class _InteractiveChartState extends State<InteractiveChart> {
  int? _selectedIndex;

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (details) {
        final index = _calculateIndex(details.localPosition);
        setState(() => _selectedIndex = index);
      },
      child: CustomPaint(
        painter: InteractiveChartPainter(
          data: widget.data,
          selectedIndex: _selectedIndex,
        ),
      ),
    );
  }
}

交互式图表允许用户点击查看具体数据点的详细信息。GestureDetector捕获触摸事件,通过触摸位置计算对应的数据点索引。选中的数据点会高亮显示,并可以展示数值标签。这种交互功能让图表从静态展示变成了可探索的数据工具,大大提升了实用性。

总结

本文详细介绍了在Flutter和OpenHarmony平台上实现打卡数据图表组件的完整方案。通过柱状图和折线图两种图表类型,用户可以从不同角度分析自己的打卡数据。动画效果让图表更加生动,交互功能让用户能够深入探索数据。两个平台都提供了强大的自定义绑制能力,Flutter使用CustomPainter,OpenHarmony使用Canvas,都能实现丰富的图表效果。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐