Flutter进阶—实现动画效果(一)

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/hekaiyou/article/details/72259392

上一篇文章我们了解了Flutter的动画基础:Flutter进阶—解析动画,这一篇文章我们就来实现一个图表的动画效果。

首先,我们需要创建一个新项目myapp,然后把main.dart的内容替换成下面的代码

import 'package:flutter/material.dart';
import 'dart:math';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // Random([int seed ]):创建一个随机数生成器
  final random = new Random();
  int dataSet;

  void changeData() {
    setState(() {
      dataSet = random.nextInt(100);
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Center(
        child: new Text('数据集:$dataSet'),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: changeData,
        child: new Icon(Icons.refresh),
      ),
    );
  }
}

启动项目后,应用程序会显示一个居中的文本标签,显示“数据集:null”和浮动按钮来刷新数据。

我们的应用程序生成的树结构如下图所示,您可以看到,虽然控件概念相当广泛,但每个具体的控件类型通常具有非常重要的责任。

这里写图片描述

通过定义用户界面的不可变的控件树,修改用户界面的唯一方法是重建树,当下一帧到期时告诉Flutter一个子树所依赖的一些状态已经改变了。这种状态依赖的子树的根必须是StatefulWidget,一个StatefulWidget不是可变的,但是它的子树是由State对象构建的。Flutter在构建期间通过树重建保留State对象并将其附加到新树中的各自的控件,然后,它们确定该控件的子树是如何构建的。在我们的应用程序中,MyHomePage是以_MyHomePageState为其状态的StatefulWidget,每当用户按下按钮时,我们执行一些代码来更改_MyHomePageState。我们已经用setState划分了这个变化,以便Flutter可以进行内部管理,并调度控件树进行重建。当发生这种情况时,_MyHomePageState将构建一个稍微不同的子树,这个子树以新的MyHomePage实例为根。

不可变的控件和状态依赖的子树是Flutter提供的主要工具,用于处理响应异步事件(比如按钮、定时器刻度或输入数据)的复杂用户界面中的状态管理的复杂性。

我们的应用程序将保持简单的控件结构,但我们会做一些动画定制图形,第一步是用一个非常简单的图表替换每个数据集的文本显示。由于数据集当前仅有一个在0~100之间数字,所以图表将是一个带有单个条形的条形图,其高度由该数字确定,我们将使用初始值50来避免高度为null。

import 'package:flutter/material.dart';
import 'dart:math';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // Random([int seed ]):创建一个随机数生成器
  final random = new Random();
  int dataSet = 50;

  void changeData() {
    setState(() {
      dataSet = random.nextInt(100);
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Center(
        child: new CustomPaint(
          size: new Size(200.0, 100.0),
          painter: new BarChartPainter(dataSet.toDouble())
        )
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: changeData,
        child: new Icon(Icons.refresh),
      ),
    );
  }
}

// CustomPaint:是将绘画委托给CustomPainter策略的控件
class BarChartPainter extends CustomPainter {
  static const barWidth = 10.0;

  BarChartPainter(this.barHeight);
  final double barHeight;

  /*
  void paint(
    Canvas canvas,
    Size size
  )
  当对象需要绘制时调用,它给出Canvas的坐标空间,使得原点位于框的左上角,
  框的面积是size参数的大小
   */
  @override
  void paint(Canvas canvas, Size size) {
    final paint = new Paint()
        ..color = Colors.blue[400]
        ..style = PaintingStyle.fill;
    // drawRect:使用给定的Paint绘制一个矩形,是否填充或描边(或两者)是由Paint.style控制
    canvas.drawRect(
      // Rect.fromLTWH(double left, double top, double width, double height):
      // 从左上角和上边缘构造一个矩形,并设置其宽度和高度
      new Rect.fromLTWH(
          size.width-barWidth/2.0,
          size.height-barHeight,
          barWidth,
          barHeight
      ),
      paint
    );
  }

  /*
  bool shouldRepaint(
    CustomPainter,
    oldDelegate
  )
  当定制绘画委托类的新实例被提供给RenderCustomPaint对象时,
  或任何时候使用自定义绘画委托类的新实例创建新的CustomPaint对象
  (这相当于同一件事,因为后者是以前者实施)
   */
  @override
  bool shouldRepaint(BarChartPainter old) => barHeight != old.barHeight;
}

下一步是添加动画,每当数据集发生变化时,我们希望该栏可以平滑而不是突然地改变高度。Flutter有一个AnimationController的概念,用于编排动画,通过注册一个监听器,我们被告知当动画值(0.0~1.0)改变时。每当发生这种情况,我们可以像以前一样调用setState并更新_MyHomePageState。

import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
import 'dart:math';
import 'dart:ui' show lerpDouble;

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  // Random([int seed ]):创建一个随机数生成器
  final random = new Random();
  int dataSet = 50;
  AnimationController animation;
  double startHeight;
  double currentHeight;
  double endHeight;

  /*
  @protected
  @mustCallSuper
  void initState()
  将此对象插入树中时调用
  该框架将为其创建的每个State对象精确地调用此方法一次
   */
  @override
  void initState() {
    super.initState();
    /*
    AnimationController({
      double value,
      Duration duration,
      String debugLabel,
      double lowerBound: 0.0,
      double upperBound: 1.0,
      TickerProvider vsync
    })
    创建动画控制器
     */
    animation = new AnimationController(
      // 这个动画应该持续的时间长短
      duration: const Duration(milliseconds: 300),
      vsync: this
    )
    /*
    void addListener(
      VoidCallback listener
    )
    每次动画值更改时调用监听器
    可以使用removeListener删除监听器
     */
      ..addListener((){
      setState((){
        /*
        double lerpDouble(
          num a,
          num b,
          double t
        )
        在两个数字之间进行线性内插
        return a + (b - a) * t;
         */
        currentHeight = lerpDouble(
          startHeight,
          endHeight,
          animation.value
        );
      });
    });
    startHeight = 0.0;
    currentHeight = 0.0;
    endHeight = dataSet.toDouble();
    // 开始向前运行这个动画(朝向最后)
    animation.forward();
  }

  /*
  @override
  void dispose()
  当该对象永久从树中删除时调用
  当该State对象永远不会再次构建时,该框架调用此方法
  框架调用dispose后,该State对象被视为已卸载,并且mounted属性为false,此时调用setState是一个错误
  生命周期的这个阶段是终点:没有办法重新安装dispose的State对象
   */
  @override
  void dispose() {
    animation.dispose();
    super.dispose();
  }

  void changeData() {
    setState(() {
      startHeight = currentHeight;
      dataSet = random.nextInt(100);
      endHeight = dataSet.toDouble();
      animation.forward(from: 0.0);
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Center(
        child: new CustomPaint(
          size: new Size(200.0, 100.0),
          painter: new BarChartPainter(currentHeight)
        )
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: changeData,
        child: new Icon(Icons.refresh),
      ),
    );
  }
}

// CustomPaint:是将绘画委托给CustomPainter策略的控件
class BarChartPainter extends CustomPainter {
  static const barWidth = 10.0;

  BarChartPainter(this.barHeight);
  final double barHeight;

  /*
  void paint(
    Canvas canvas,
    Size size
  )
  当对象需要绘制时调用,它给出Canvas的坐标空间,使得原点位于框的左上角,
  框的面积是size参数的大小
   */
  @override
  void paint(Canvas canvas, Size size) {
    final paint = new Paint()
        ..color = Colors.blue[400]
        ..style = PaintingStyle.fill;
    // drawRect:使用给定的Paint绘制一个矩形,是否填充或描边(或两者)是由Paint.style控制
    canvas.drawRect(
      // Rect.fromLTWH(double left, double top, double width, double height):
      // 从左上角和上边缘构造一个矩形,并设置其宽度和高度
      new Rect.fromLTWH(
          size.width-barWidth/2.0,
          size.height-barHeight,
          barWidth,
          barHeight
      ),
      paint
    );
  }

  /*
  bool shouldRepaint(
    CustomPainter,
    oldDelegate
  )
  当定制绘画委托类的新实例被提供给RenderCustomPaint对象时,
  或任何时候使用自定义绘画委托类的新实例创建新的CustomPaint对象
  (这相当于同一件事,因为后者是以前者实施)
   */
  @override
  bool shouldRepaint(BarChartPainter old) => barHeight != old.barHeight;
}

上面代码中的lerpDouble函数比较难理解,代入参数之后计算结果如下图。

这里写图片描述

数据从一开始的0.0到达50.0时,花费了10个时间点。再到达52时,则花费了16个时间点。因此大约得出的结论时,在我们的应用程序中,数据变化越小,花费的时间点越多。

这里写图片描述

现在程序已经变得复杂性,我们的数据集仍然只是一个数字,设置动画控制所需的代码是一个小问题,因为当我们获得更多的图表数据时,它不会被分解。真正的问题是变量startHeight、currentHeight和endHeight,反映了对数据集和动画值所做的更改,并在三个不同的地方更新。

我们需要一个概念来处理这个混乱的情况。

未完待续~~~

展开阅读全文

没有更多推荐了,返回首页