Flutter快速实现自定义折线图,支持数据改变过渡动画

这篇博客展示了如何在Flutter中创建一个自定义的LineChartPainter类,用于绘制折线图。通过实现CustomPainter,文章详细解释了绘制外边框、x轴、y轴标签和数据点的过程。此外,还介绍了如何利用ImplicitlyAnimatedWidget实现数据更新时的过渡动画,包括Tween和lerp方法的应用,确保数据变化时平滑过渡。最后,给出了一个实际的使用示例,展示了如何切换不同数据集并观察动画效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最终效果如下:
在这里插入图片描述

绘制

先创建一个CustomPainter,使用canvas绘制。

import 'dart:ui';

import 'package:flutter/material.dart';

/// 自定义折线图
class LineChartPainter extends CustomPainter {
  Paint _outlinePaint;
  Paint _axisXPaint;
  Paint _valuePaint;

  /// 左边Y轴标签
  TextPainter _leftLabelPainter;
  List<int> _yTitle = [40, 30, 20, 10, 0, -10]; // y轴刻度数量
  List<double> _yData;
  final int averageY = 5; // y轴平均份数
  final int gapY = 10; // y轴平均间隔值
  final double _yTitlePadding = 5;

  LineChartPainter(this._yData, {Listenable repaint})
      : super(repaint: repaint) {
    _outlinePaint = Paint()
      ..color = Colors.grey
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1;
    _axisXPaint = Paint()
      ..color = Colors.grey
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1;
    _valuePaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.5;
    _leftLabelPainter = TextPainter()..textDirection = TextDirection.ltr;
  }

  @override
  void paint(Canvas canvas, Size size) {
    print("${this},,,$size");

    /// 外边框矩形
    canvas.drawRect(
        Rect.fromLTRB(0, 0, size.width, size.height), _outlinePaint);

    /// x轴
    for (var i = 0; i < 4; i++) {
      canvas.drawLine(Offset(0, size.height / averageY * (i + 1)),
          Offset(size.width, size.height / averageY * (i + 1)), _axisXPaint);
    }

    /// y轴标题
    for (var i = 0; i <= 5; i++) {
      _leftLabelPainter.text = TextSpan(
          text: _yTitle[i].toString(), style: TextStyle(color: Colors.black));
      _leftLabelPainter.layout();
      _leftLabelPainter.paint(
        canvas,
        Offset(-_leftLabelPainter.width - _yTitlePadding,
            size.height / 5 * i - _leftLabelPainter.height / 2),
      );
    }

    /// 数据
    /// 需要将y轴数据转换为height中对应比例的高度。
    var points = <Offset>[];
    for (var i = 0; i < _yData.length; i++) {
      points.add(Offset(size.width / _yData.length * i,
          translateValue(size.height, _yData[i])));
    }
    canvas.drawPoints(PointMode.polygon, points, _valuePaint);
  }

  @override
  bool shouldRepaint(LineChartPainter oldDelegate) {
    return oldDelegate._yData != _yData;
  }

  /// 转换y坐标
  double translateValue(double height, double rawValue) {
    /// y轴真实值总长度
    var valueSum = averageY * gapY;

    /// 真实值和height计算比例
    var scale = height / valueSum;
    var result = rawValue * scale;
    return height - result - height / averageY;
  }
}

实现过渡动画

使用CustomPaint包裹CustomPainter,为了在数据变化时,有动画效果,使用ImplicitlyAnimatedWidget。
实现动画效果的重点是forEachTweenlerp方法。
注意:State不要继承ImplicitlyAnimatedWidgetState,直接继承AnimatedWidgetBaseState即可,它内部实现了对AnimationController的监听,并实时刷新Animation的value,自动调用forEachTween方法。

import 'dart:ui';

import 'package:flutter/material.dart';

import 'line_chart_painter.dart';

/// 包装折线图 数据更新时展示过渡动画
class LineChart extends ImplicitlyAnimatedWidget {
  final Size size;
  final LineChartData lineChartData;
  final Duration duration;

  const LineChart(
      {Key key,
      this.size,
      @required this.lineChartData,
      @required this.duration})
      : super(key: key, duration: duration);

  @override
  _CustomCanvasState createState() {
    return _CustomCanvasState();
  }
}

class _CustomCanvasState extends AnimatedWidgetBaseState<LineChart> {
  DataTween _dataTween;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraint) {
        print(
            "widget size:${widget.size}  ${constraint.maxWidth}...${constraint.maxHeight}");
        var constrainSize =
            widget.size ?? Size(constraint.maxWidth, constraint.maxHeight);
        if (constrainSize.width == double.infinity) {
          throw FlutterError("必须为组件或父widget设置一个有效宽度");
        }
        if (constrainSize.height == double.infinity) {
          throw FlutterError("必须为组件或父widget设置一个有效高度");
        }
        return CustomPaint(
          foregroundPainter: LineChartPainter(
              _dataTween.evaluate(animation).data,
              repaint: animation),
          size: constrainSize,
        );
      },
    );
  }

  @override
  void forEachTween(TweenVisitor visitor) {
    /// 第一个参数是初始的tween,第二个参数是目标值,第三个是生成tween的回调。
    _dataTween = visitor(
        _dataTween, widget.lineChartData, (value) => DataTween(begin: value));
  }
}

class DataTween extends Tween<LineChartData> {
  DataTween({LineChartData begin, LineChartData end})
      : super(begin: begin, end: end);

  @override
  LineChartData lerp(double t) => LineChartData.lerp(begin, end, t);
}

class LineChartData {
  List<double> data;

  LineChartData({this.data});

  /// 计算动画更新时的数据
  /// begin表示动画开始时的数据,end是结束时的数据,t是动画估值器,从0到1,代表动画运行的进度。
  static LineChartData lerp(LineChartData begin, LineChartData end, double t) {
    /// 根据begin和end每个对应的值,因为值类型是double,所以使用系统自带的lerpDouble来计算值。
    LineChartData result;
    if (begin.data != null &&
        end.data != null &&
        begin.data.length == end.data.length) {
      result = LineChartData(
          data: List.generate(begin.data.length, (index) {
        return lerpDouble(begin.data[index], end.data[index], t);
      }));
    } else if (begin.data.length > end.data.length) {
      result = LineChartData(
          data: List.generate(end.data.length, (index) {
        return lerpDouble(begin.data[index], end.data[index], t);
      }));
    } else if (begin.data.length < end.data.length) {
      result = LineChartData(
          data: List.generate(begin.data.length, (index) {
        return lerpDouble(begin.data[index], end.data[index], t);
      }));
    }
    return result;
  }
}

使用

class StudyCanvasPage extends StatefulWidget {
  const StudyCanvasPage({Key key}) : super(key: key);

  @override
  _StudyCanvasPageState createState() => _StudyCanvasPageState();
}

class _StudyCanvasPageState extends State<StudyCanvasPage> {
  var check = false;
  List<double> _yData = [29.2, 29.8, 29.3, 29.2, 29.4, 29.1, 29.3, 29.2, 29.5, 29.2, 29.2, 29, 29.3, 29.2, 29.4, 29.1, 29.3, 29.2, 29.5, 29.2];
  List<double> _yData1 = [12.2, 19.8, 19.3, 9.2, 19.4, 19.1, 19.3, 19.2, 19.5, 19.2, 19.2, 19, 19.3, 19.2, 19.4, 19.1, 19.3, 19.2, 19.5, 19.2,];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('study canvas'),
      ),
      body: SafeArea(
        child: Container(
          child: Column(
            children: [
              ElevatedButton(
                onPressed: () {
                  setState(() {
                    check = !check;
                  });
                },
                child: Text("切换数据"),
              ),
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.all(28.0),
                  child: LineChart(
                    lineChartData:
                        LineChartData(data: check ? _yData : _yData1),
                    duration: Duration(milliseconds: 300),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值