Flutter 动画入门

作者能力有限, 如果您在阅读过程中发现任何错误, 还请您务必联系本人,指出错误, 避免后来读者再学习错误的知识.谢谢!

参考: https://flutter.dev/docs/development/ui/animations/tutorial

基本概念

Animation: Flutter 提供的核心动画库, 使用指定的值来构建动画.

Animation 实例: 它只知道动画当前的状态(state)(比如,动态是否启动,结束,或者正在执行), 但是不知道当前屏幕显示的内容.

CurveAnimation: 用于定义一个非线性曲线.

Tween: 补间动画,用于在动画状态之间插入补充的值。 比如,它可以用与在从红色变成绿色的动画中插入中间值.

ListensersStatusListeners: 用于监听动画状态(state)的改变.

下面简要介绍一下常用的 Animation.

Animation<double>

Animation 的实例会在给定的两个值之间生成连续的中间值。 生成数据的间隔由 duration 决定. 生成连续值的方式可以有以下几种: 线性,曲线,阶跃,或者其他方法.

Animation 实例是有状态的, 可以通过 .value 字段获取它当前的状态值.

Animtation 实例无法知道 build 函数的状态和其他绘制状态.

CurvedAnimation

CurvedAnimation 用于定义一个非线性动画.

animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);

Curves 类中定义了许多常用的曲线

CurvedAnimation 和 AnimationController 类型都是 Animation 的子类型. 因此你可以将他们赋值给 Animation.

AnimationController

AnimationController 是一个特殊的 Animation, 用户生成新的值在硬件准备好之后. 默认情况下, 它会已特定间隔生成一系列间与 0.0 - 1.0 的值.

例如:

controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);

正如前文所说,AnimationController 是 Animation 的子类,相比于基类,它提供了一些额外的方法来控制动画:
比如,你可以使用 forward() 来开始一段动画.

生成数据的频率由屏幕刷新次数决定. 通常情况下是每分钟 60 次.

每当新数据生成时,每个 Animation 实例的 listenser 会被调用.

当创建一个动画对象时,需要传入一个 vsync 参数. 使用 vsync 可以防止屏幕外的动画消耗不必要的资源。

Tween

通常情况下, AnimationController 实例的值范围为 0.0 - 1.0. 当你需要使用其他范围的值时, 你可以通过使用 Tween 来配置动态来生成不同范围的值.

比如: 当你想指定值的范围为 -200 - 0.0

tween = Tween<double>(begin: -200, end: 0);

Tween 是一个无状态的对象(stateless), 它仅仅知道 begin 和 end. 它的任务通常是用来将一个值范围映射为另外一个值范围. 被映射的值范围通常都是 0.0 - 1.0.

Tween 是 Animatable 的子类,注意不是 Animation. Animatable 不需要生成的 double 值的输出.

比如: ColroTween 用于生成在两个 Color 之间的动画.

colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);

Tween 实例不保存任何状态,但是它提供了 evalute(Animation animation) 方法. 这个方法可以用于映射当前动画值. 当前动画值可以通过 Animation 实例的 value 字符访问.

Tween.animate

可以通过将 controller 对象传给 Tween.animate 方法来使用一个 Tween.
比如: 如下代码在 500 ms 中生成 0 - 255 之间的值.

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
// 注意,animate 返回一个 Animation 对象,而不是 Animatable 对象.
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

Animation Notification

Animation 实例可以由它的 Listenser 和 StatusListener,分别通过 addListener()addStatusListener() 方法添加.

Listenser 将会被回调当动画值改变的时候. 最常见的做法是在 Listener 中调用 setState() 来重绘页面.
StatusListener 将会被调用当动画开始, 结束,前进,后退的时候.

动画实例

Rendering animations

一个显示动画 Flutter Logo 的实例

原始代码,无动画

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: 300,
        width: 300,
        child: FlutterLogo(),
      ),
    );
  }
}

接下来,修改代码,让这个 logo 从没有到占满屏幕

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {

  Animation<double>   animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addListener(() {
        setState(() {});
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

我们使用 addListener 调用 setState

每次 Animation 生成一个新的值之后会调用我们添加的 listener, 通过调用 setState 将当前帧标记为 dirty, 之后 Flutter 就会重绘整个帧.

在 build 方法中, 将 container 的 height 和 width 指定为 animation 当前的值,这样每次动画值更新后,container 的大小就会跟着变化.

在 initState 中初始话 controller, 在 dispose 中释放 controller, 以避免内存泄漏.

效果如下:

在这里插入图片描述

Simplifying with Animated­Widget

这里我们使用 AnimatedWidget 来简化上一步的代码, 将动画代码从 widget 的构建代码中分离出来。

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {

  Animation<double>   animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

class AnimatedLogo extends AnimatedWidget {
  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }
}

Monitoring the progress of the animation

本实例将使用 addStatusListener 来监听动画的状态, 实现一个呼吸动画.

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {

  Animation<double>   animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      })
    ..addStatusListener((status) => print('$status'));
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

class AnimatedLogo extends AnimatedWidget {
  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }
}

主要添加了 18-36 行的代码.

效果如下:

在这里插入图片描述

Refactoring with AnimatedBuilder

上面代码中存在一个问题, 那就是动画改变时会同时修改包含它的页面.

一个更好的解决方案是将绘制动画的逻辑由另外一个类型负责.

我们使用 AnimatedBuilder 类来完成该功能. 就像 AnimatedWidget 一样, AnimatedBuilder 会自动监听动画的状态, 标记当前 widget
为 dirty 在适当的时候, 然后重绘动画. 我们不再需要调用 addListener.

为此,我们就构建如下的 widget tree:

在这里插入图片描述

LogoWidget 的实现非常简单, 仅仅显示一个 Flutter Logo.

我们在 GrowTransition 的 build 方法中构建中间的 widgets.

完整代码如下:

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {

  Animation<double>   animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return GrowTransition(
      child: LogoWidget(),
      animation: animation,
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

class LogoWidget extends StatelessWidget {
  // Leave out the height and width so it fills the animating parent
  Widget build(BuildContext context) => Container(
    margin: EdgeInsets.symmetric(vertical: 10),
    child: FlutterLogo(),
  );
}

class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) => Center(
    /*
	* One tricky point in the code below is that the child looks like it’s specified twice.
	* What’s happening is that the outer reference of child is passed to AnimatedBuilder,
	* which passes it to the anonymous closure, which then uses that object as its child.
	* The net result is that the AnimatedBuilder is inserted in between the two widgets in the render tree.
	*/
    child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) => Container(
          height: animation.value,
          width: animation.value,
          child: child,
        ),
        child: child),
  );
}

Simultaneous animations

这一节,我们在 “monitoring the progress of the animation” 小节的基础上构建一个同时变化的动画.

我们的 Logo 动画将在改变大小的同时改变自身的透明度.

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {

  Animation<double>   animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

class AnimatedLogo extends AnimatedWidget {
  // Make the Tweens static because they don't change.
  static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
  static final _sizeTween    = Tween<double>(begin: 0, end: 300);

  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child:  Opacity (
        opacity: _opacityTween.evaluate(animation), // 通过evaluate 获取 Tween 当前的值
        child: Container(
          margin: EdgeInsets.symmetric(vertical: 10),
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: FlutterLogo(),
        ),
      ),
      
    );
  }
}

效果如下:

在这里插入图片描述

END!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章