基本概念

Animation,Flutter 动画库中的核心类,插入用于指导动画的值。

Animation 对象知道动画目前的状态(例如,是否开始,暂停,前进或倒退),但是对屏幕上显示的内容一无所知。

AnimationController 管理 Animation。

CurvedAnimation 定义动画进度为非线性曲线。

Tween 为动画对象插入一个范围值。例如,Tween 可以定义插入值由红到蓝,或从 0 到 255。

使用 Listeners 和 StatusListeners 监视动画状态变化。

Animation<double>

Animation是一个已知当前值和状态(已完成或已解除)的抽象类,它的当前值使用.value获取。一个 Animation 对象在一段时间内,持续生成介于两个值之间的插入值。这个 Animation 对象输出的可能是直线,曲线,阶梯函数,或者任何自定义的映射。根据 Animation 对象的不同控制方式,它可以反向运行,或者中途切换方向。

Curved­Animation

CurvedAnimation 和 AnimationController都是 Animation<double> 类型,所以可以互换使用。CurvedAnimation 封装正在修改的对象 — 不需要将 AnimationController 分解成子类来实现曲线。

Tween

在默认情况下,AnimationController 对象的范围是 0.0-0.1。如果需要不同的范围或者不同的数据类型,可以使用 Tween 配置动画来插入不同的范围或数据类型。例如下面的示例中,Tween 的范围是 -200 到 0.0。

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

Tween是无状态的,只有begin和end,Tween的作用只是为了定义输入范围(通常是0.0-1.0,但也不一定)到输出范围的映射。
Tween 对象不存储任何状态。而是提供 evaluate(Animation<double> animation) 方法,将映射函数应用于动画当前值。

ColorTween 指定了两种颜色之间的过程:

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

要使用 Tween 对象,请在 Tween 调用 animate(),传入控制器对象。animate() 方法会返回一个 Animation。
例如,下面的代码在 500 ms 的进程中生成 0-255 范围内的整数值 :

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

Tween<T extends dynamic>继承自Animatable<T>,其实现类有:

https://api.flutter-io.cn/flutter/animation/Tween-class.html
AlignmentGeometryTween, AlignmentTween, BorderRadiusTween, BorderTween,BoxConstraintsTween, ColorTween, ConstantTween, DecorationTween,EdgeInsetsGeometryTween,EdgeInsetsTween,FractionalOffsetTween,IntTween,MaterialPointArcTween,Matrix4Tween,RectTween,RelativeRectTween,ReverseTween,ShapeBorderTween,SizeTween,StepTween,TextStyleTween, ThemeDataTween

Animatable 类介绍

Animatable是这样的一个对象,它在输入Animation <double>作为输入的情况下,可以产生T类型值。
通常,输入动画的值通常在0.0到1.0的范围内。 但是,原则上可以提供任何值。

通常,输入动画的值通常在0.0到1.0的范围内。 但是,原则上可以提供任何值。

下面的示例展示了一个控制器AnimationController,一个曲线Curve,和一个 Tween的组合使用:

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
    CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

使用动画

渲染动画

要显示一个 Animation 对象,需将 Animation 对象存储为您的 widget 成员,然后用它的值来决定如何绘制。

比如显示一个Flutter Logo的动画,在使用Animation之前,代码如下:

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(),
      ),
    );
  }
}

在使用Animation的值来改变Widget的属性,这里是Container的height和width,代码修改后如下:

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(() {    
                    // The state that has changed here is the animation object’s value.    
                     });    
                   });    
             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();    
           }    
   }

使用 Animated­Widget 进行简化

关于Animated­Widget,在之前的一篇文章中已经做过介绍:


如何使用 AnimatedWidget 帮助类(代替 addListener() 和 setState())创建动画 widget
利用 AnimatedWidget 创建一个可以运行重复使用动画的 widget

AnimatedWidget 基本类可以从动画代码中区分出核心 widget 代码。 AnimatedWidget 不需要保持 State 对象来 hold 动画。
AnimatedWidget对于无状态的Widget最有用。 要使用AnimatedWidget,只需将其子类化并实现build函数。

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(),
      ),
    );
  }
}

在使用AnimatedWidget将动画逻辑封装到自定义的Widget后,相应的调用方式代码修改如下:

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);
        }

        @override    
        Widget build(BuildContext context) => AnimatedLogo(animation: animation);   
   
        @override    
        void dispose() {    
             controller.dispose();    
             super.dispose();    
           }    
 }

AnimatedBuilder

AnimatedBuilder 知道如何渲染过渡效果

在上面使用继承AnimatedWidget来封装Logo时,代码中有个问题,就是改变动画需要改变渲染 logo 的widget。

较好的解决办法是,将任务区分到不同类里:

  • 渲染logo Render the logo
  • 定义动画对象 Define the Animation object
  • 渲染过渡效果 Render the transition

这时候可以将要产生动画的Widget的渲染逻辑单独到一个类中;然后自定义一个GrowTransition类,封装了AnimatedBuilder,
AnimatedBuilder的参数animation传入动画对象(通常是一个AnimationController对象)和一个builder,在这里builder里指定过渡效果的渲染逻辑,第三个参数就是要产生动画效果的Widget对象。

全部代码如下:

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

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

// #docregion LogoWidget
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(),
      );
}
// #enddocregion LogoWidget

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

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) => Center(
        child: AnimatedBuilder(
            animation: animation,
            builder: (context, child) => Container(
                  height: animation.value,
                  width: animation.value,
                  child: child,
                ),
            child: child),
      );
}
// #enddocregion GrowTransition

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

// #docregion print-state
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();
  }
  // #enddocregion print-state

  @override
  Widget build(BuildContext context) => GrowTransition(
        child: LogoWidget(),
        animation: animation,
      );

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
  // #docregion print-state
}

如同在使用TweenAnimationBuilder中使用child参数来传入一些不发生改变的Widget来进行性能优化,在AnimatedBuilder中,也可以使用child参数来传入不发生改变的Widget来进行性能优化

这里本来是对FlutterLogo的宽高显示效果进行动画,但是为了性能优化,这里转换了思路,对FlutterLogo的外层Container进行宽高动画,而FlutterLogo采取充满父Widget来达到相同的视觉效果,这样在Widget比较复杂时,可以避免动画过程中重复多次创建Widget(比如这里的FlutterLogo),以此来提高性能。

AnimatedBuilder VS AnimatedWidget

如果在内置的显式动画FooTransition中找不到合适的Widget,那么可以使用AnimatedBuilder 和继承AnimatedWidget的方式来自定义显式动画,那如何在二者之中选择呢?

通常建议使用继承AnimatedWidget的方式,不过如果如果创建controller的父Widget非常简单,则仅为动画制作单独的独立Widget可能会产生太多额外的代码。 在这种情况下,AnimatedBuilder就是您所需要的。当然上面说到的性能优化点也是可以一起考虑来进行选择的。

使用一个controller和多个Tween同时进行动画

每个补间动画控制一个动画的不同方面,例如:

controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);
sizeAnimation = Tween<double>(begin: 0, end: 300).animate(controller);
opacityAnimation = Tween<double>(begin: 0.1, end: 1).animate(controller);

全部代码如下:

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

// #docregion diff
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),
        child: Container(
          margin: EdgeInsets.symmetric(vertical: 10),
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: FlutterLogo(),
        ),
      ),
    );
  }
}

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

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    // #docregion AnimationController, tweens
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    // #enddocregion AnimationController, tweens
    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();
  }
}

参考视频和博文:

Making your first directional animations with built-in explicit animations

Directional animations with built-in explicit animations

Creating custom explicit animations with AnimatedBuilder and AnimatedWidget

When should I useAnimatedBuilder or AnimatedWidget?

如果觉得我的文章对你有用,请随意赞赏