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

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

最终效果如下:

绘制

先创建一个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),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}


相关文章
|
15天前
深入理解Flutter鸿蒙next版本 中的Widget继承:使用extends获取数据与父类约束
本文详细介绍了Flutter中如何通过继承其他Widget来创建自定义组件。首先解释了Widget继承的基本概念,包括StatelessWidget和StatefulWidget的区别。接着通过具体示例展示了如何继承StatelessWidget和StatefulWidget,并在子类中访问父类的build方法和状态。最后,结合多个自定义Widget展示了如何在实际应用中灵活使用继承和组合来构建复杂的UI。
66 8
|
13天前
|
开发工具 UED 容器
Flutter&鸿蒙next 实现长按录音按钮及动画特效
本文介绍了如何在 Flutter 中实现一个带有动画效果的长按录音按钮。通过使用 `GestureDetector` 监听长按手势,结合 `AnimatedContainer` 和 `AnimationController` 实现按钮的动画效果,以及 `flutter_sound` 插件完成录音功能。文章详细讲解了功能需求、实现思路和代码实现,帮助读者逐步掌握这一实用功能的开发方法。
90 5
|
13天前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
74 1
|
15天前
Flutter 自定义组件继承与调用的高级使用方式
本文深入探讨了 Flutter 中自定义组件的高级使用方式,包括创建基本自定义组件、继承现有组件、使用 Mixins 和组合模式等。通过这些方法,您可以构建灵活、可重用且易于维护的 UI 组件,从而提升开发效率和代码质量。
111 1
|
15天前
|
JavaScript API 开发工具
<大厂实战场景> ~ Flutter&鸿蒙next 解析后端返回的 HTML 数据详解
本文介绍了如何在 Flutter 中解析后端返回的 HTML 数据。首先解释了 HTML 解析的概念,然后详细介绍了使用 `http` 和 `html` 库的步骤,包括添加依赖、获取 HTML 数据、解析 HTML 内容和在 Flutter UI 中显示解析结果。通过具体的代码示例,展示了如何从 URL 获取 HTML 并提取特定信息,如链接列表。希望本文能帮助你在 Flutter 应用中更好地处理 HTML 数据。
93 1
|
15天前
|
前端开发 开发者
深入探索 Flutter 鸿蒙版的画笔使用与高级自定义动画
本文深入探讨了 Flutter 中的绘图功能,重点介绍了 CustomPainter 和 Canvas 的使用方法。通过示例代码,详细讲解了如何绘制自定义图形、设置 Paint 对象的属性以及实现高级自定义动画。内容涵盖基本绘图、动画基础、渐变动画和路径动画,帮助读者掌握 Flutter 绘图与动画的核心技巧。
64 1
|
15天前
|
Dart UED 开发者
Flutter&鸿蒙next中的按钮封装:自定义样式与交互
在Flutter应用开发中,按钮是用户界面的重要组成部分。Flutter提供了多种内置按钮组件,但有时这些样式无法满足特定设计需求。因此,封装一个自定义按钮组件变得尤为重要。自定义按钮组件可以确保应用中所有按钮的一致性、可维护性和可扩展性,同时提供更高的灵活性,支持自定义颜色、形状和点击事件。本文介绍了如何创建一个名为CustomButton的自定义按钮组件,并详细说明了其样式、形状、颜色和点击事件的处理方法。
64 1
|
15天前
|
Dart 搜索推荐 API
Flutter & 鸿蒙next版本:自定义对话框与表单验证的动态反馈与错误处理
在现代移动应用开发中,用户体验至关重要。本文探讨了如何在 Flutter 与鸿蒙操作系统(HarmonyOS)中创建自定义对话框,并结合表单验证实现动态反馈与错误处理,提升用户体验。通过自定义对话框和表单验证,开发者可以提供更加丰富和友好的交互体验,同时利用鸿蒙next版本拓展应用的受众范围。
65 1
|
21天前
动画控制器在 Flutter 中的工作原理
【10月更文挑战第18天】总的来说,动画控制器 `AnimationController` 在 Flutter 中起着关键的作用,它通过控制动画的数值、速度、节奏和状态,实现了丰富多彩的动画效果。理解它的工作原理对于我们在 Flutter 中创建各种精彩的动画是非常重要的。
|
15天前
|
JSON Dart 数据格式
<大厂实战场景> ~ flutter&鸿蒙next处理后端返回来的数据的转义问题
在 Flutter 应用开发中,处理后端返回的数据是常见任务,尤其涉及转义字符时。本文详细探讨了如何使用 Dart 的 `dart:convert` 库解析包含转义字符的 JSON 数据,并提供了示例代码和常见问题的解决方案,帮助开发者有效处理数据转义问题。
110 0