Flutter了解之入门篇13(动画)

目录

  前言
  1. 重要的类(Animation、Curve、AnimationController、Tween)
  2. 用AnimatedWidget简化、用AnimatedBuilder重构、监听状态
  3. 自定义路由切换动画
  4. Hero动画(又称:共享元素转换)
  5. 交织动画(Stagger Animation)
  6. AnimatedSwitcher(切换组件动画---实现页面内的场景切换)
  7. 动画过渡组件

前言

动画可以提升用户体验。

原理
  在任何系统的UI框架中,动画实现的原理都和电影的原理一样,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画。

帧
  将UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数。
  帧率越高则动画就会越流畅。一般情况下,对于人眼来说,动画帧率超过16FPS,就比较流畅了,超过32FPS就会非常的细腻平滑,而超过32FPS,人眼基本上就感受不到差别了。
  由于动画的每一帧都是要改变UI输出,所以在一个时间段内连续的改变UI输出是比较耗资源的,对设备的软硬件系统要求都较高,所以在UI系统中,动画的平均帧率是重要的性能指标。而在Flutter中,理想情况下是可以实现60FPS的,这和原生应用能达到的帧率是基本是持平的。
  1. 动画类型(2种)
1. 基于tween(补间动画)。
  定义了开始点和结束点、时间线以及定义转换时间和速度的曲线,然后从开始点过渡到结束点。
2. 基于物理(物理动画)。
  运动被模拟为与真实世界的行为。
  如:掷球时,它在何处落地,取决于抛球速度有多快、球有多重、距离地面有多远。 
  1. 动画模式
1. 动画列表或网格
  涉及在网格或列表中添加或删除元素时应用动画
2. 共享元素转换(Hero动画)
  用户从页面中选择一个元素(通常是一个图像),然后打开所选元素的详情页面,在打开详情页时使用动画。
3. 交错动画
  动画被分解为较小的动作,其中一些动作被延迟。较小的动画可以是连续的,或者可以部分或完全重叠。

示例(3个叠加的小爱心图标 放大缩小效果)

import 'package:flutter/material.dart';
class AnimtionDemo extends StatefulWidget {
  const AnimtionDemo({Key? key}) : super(key: key);
  @override
  _AnimtionDemoState createState() => _AnimtionDemoState();
}
class _AnimtionDemoState extends State<AnimtionDemo>
    with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;
  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 1), vsync: this);
    animation = Tween<double>(begin: 40, end: 100).animate(controller)
      ..addListener(() {
        setState(() {});
      });
    controller.addStatusListener((status) {
      print(status);
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Animation 动画'),
      ),
      body: Center(
        child: Stack(
          alignment: Alignment.center,
          children: [
            Icon(
              Icons.favorite,
              color: Colors.red[100],
              size: animation.value * 1.5,
            ),
            Icon(
              Icons.favorite,
              color: Colors.red[400],
              size: animation.value,
            ),
            Icon(
              Icons.favorite,
              color: Colors.red[600],
              size: animation.value / 2,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow, color: Colors.white),
        onPressed: () {
          if (controller.status == AnimationStatus.completed) {
            controller.reverse();
          } else {
            controller.forward();
          }
        },
      ),
    );
  }
  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

1. 重要的类

  1. Animation(抽象类)

它本身和UI渲染没有任何关系,主要用来保存动画的插值和状态。
在一段时间内依次生成一个区间值的类(通常60个值/秒,以达到60fps的效果)。

/*
常用:
  1. Animation<double>
    CurvedAnimation和AnimationController都继承自Animation<double>。
  2. Animation<Color> 
  3. Animation<Size>
*/
常用属性和方法:
  1. status属性(获取当前状态)、value属性(获取当前值)
  2. void addListener(VoidCallback listener);  
    值改变后会调用(可以在这里调用setState方法来触发UI重建)。
    可通过removeListener()移除监听。
  3. addStatusListener();
    状态(开始、结束、正向、反向)改变后会调用。
    可通过removeStatusListener()移除监听。
/*
动画状态AnimationStatus枚举(4种)
  dismissed 动画在起始点停止
  forward 动画正在正向执行
  reverse 动画正在反向执行
  completed 动画在终点停止
*/
  1. Curve 曲线

决定Animation对象在动画执行过程中输出的值是线性的(匀速)、曲线的(非匀速)、步进函数或任何曲线函数。

// 通过CurvedAnimation来指定动画的曲线。CurvedAnimation通过包装AnimationController和Curve生成一个新的动画对象,将动画和动画执行曲线关联起来。
// CurvedAnimation和AnimationController都是Animation<double>类型。
final CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
/*
Curves枚举类
  linear 匀速
  decelerate 匀减速
  ease 先加速后减速
  easeIn 由慢到快
  easeOut 由快到慢
  easeInOut 由慢到快再到慢
  ...

自定义动画曲线Curve。例(定义一个正弦曲线):
class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2);
    // return sin(2 * (count + 0.25) * pi * t); 补偿pi/2个角度使终点值是1,否则最后会突然跳回原位置。count控制周期。
  }
}
*/

自定义动画曲线

首先看一下Curve的定义:
// 重载transform方法(起点值0.0和终点值1.0不用被转换函数转换)。
abstract class Curve extends ParametricCurve<double> {
  const Curve();
  @override
  double transform(double t) {
    if (t == 0.0 || t == 1.0) {
      return t;
    }
    return super.transform(t);
  }
  Curve get flipped => FlippedCurve(this);
}

继续看一下ParametricCurve的定义:
abstract class ParametricCurve<T> {
  const ParametricCurve();
  T transform(double t) {
    // 合法性检验(参数t不能为null 且 范围必须在0-1之间)
    assert(t != null);
    assert(t >= 0.0 && t <= 1.0, 'parametric value $t is outside of [0, 1] range.');
    return transformInternal(t);
  }
  @protected
  // 子类必须要实现该方法,否则会抛出UnimplementedError异常。
  // 返回t点对应的曲线值
  T transformInternal(double t) {
    throw UnimplementedError();
  }
  @override
  String toString() => objectRuntimeType(this, 'ParametricCurve');
}

接着看Curves.linear的实现:
// 对应的数学函数:y = f(t) = t
class _Linear extends Curve {
  const _Linear._();
  @override
  double transformInternal(double t) => t;
}
再接着看Curves.decelerate的实现:
// 对应的数学函数:y = f(t) = 1-(1-t)*(1-t) = 2*t - t*t
class _DecelerateCurve extends Curve {
  const _DecelerateCurve._();
  @override
  double transformInternal(double t) {
    t = 1.0 - t;
    return 1.0 - t * t;
  }
}

从上面可知,动画曲线的机制:
  在给定的Duration动画时间内,完成组件的初始状态到结束状态的转变,这个转变是沿着设定的 Curve类完成的,其横坐标是0-1,曲线的初始值和结束值分别是0和1(中间值可超出)。

示例(CurvedAnimation、AnimatedWidget、监听状态)

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
void main() {
  runApp(new LogoApp());
}
class LogoApp extends StatefulWidget {
  _LogoAppState createState() => new _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
  // 要使用Animation<>对象进行渲染,将Animation对象存储为Widget的成员,然后使用其value值来决定如何绘制
  Animation<double> animation;
  AnimationController controller;
  initState() {
    super.initState();
    // 创建controller
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    // 创建animation对象
    animation = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
    // 监听
    controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        controller.forward();
      }
    });
    // 开启动画
    controller.forward();
  }
  Widget build(BuildContext context) {
    return new AnimatedLogo(animation: animation);
  }
  dispose() {
    // 销毁controller
    controller.dispose();
    super.dispose();
  }
}
// 使用AnimatedWidget简化,不用在addListener监听方法中使用setState((){})来更新UI
class AnimatedLogo extends AnimatedWidget {
  // 创建Tween,每一个Tween管理动画的一种效果
  static final _opacityTween = new Tween<double>(begin: 0.1, end: 1.0);
  static final _sizeTween = new Tween<double>(begin: 0.0, end: 300.0);
  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: new Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: new Container(
          margin: new EdgeInsets.symmetric(vertical: 10.0),
          height: _sizeTween.evaluate(animation),    // 获取动画的当前值
          width: _sizeTween.evaluate(animation),
          child: new FlutterLogo(),
        ),
      ),
    );
  }
}

代码存在的一个问题: 更改动画需要更改显示logo的widget。更好的解决方案是将职责分离:
    1. 显示logo(UI)
    2. 定义Animation对象
    3. 渲染过渡效果
使用AnimatedBuilder:

_LogoAppState的build方法改为:
  Widget build(BuildContext context) {
    // Animation对象在_LogoAppState中创建,并在这里传入GrowTransition组件
    return new GrowTransition(child: new LogoWidget(), animation: animation);
  }
// 1. 显示logo
class LogoWidget extends StatelessWidget {
  build(BuildContext context) {
    return new Container(
      margin: new EdgeInsets.symmetric(vertical: 10.0),
      child: new FlutterLogo(),
    );
  }
}
// 3. 渲染过渡效果
class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});
  final Widget child;
  final Animation<double> animation;
  Widget build(BuildContext context) {
    return new Center(
      child: new AnimatedBuilder(
          animation: animation,
          builder: (BuildContext context, Widget child) {
            return new Container(
                height: animation.value, width: animation.value, child: child);
          },
          child: child),
    );
  }
}
  1. AnimationController (继承自 Animation<double>)

用于控制动画:启动forward()、停止stop() 、反向reverse()等。
在动画的每一帧,会根据动画曲线来生成当前的动画值去构建UI。

  AnimationController({
    double? value,
    // 动画时长,用来控制动画的速度
    this.duration,  
    this.reverseDuration,
    this.debugLabel,
    // 使用了Tween则需要在Tween中设置范围,不再需要在这设置。
    // 生成值的范围由lowerBound和upperBound决定。默认[0.0,1.0]。
    // 某些情况下会超出范围(取决于曲线函数,如fling方法据手指滑动/甩出的速度、力量等来模拟一个手指甩出动画,可能会超出范围。还有Curves.elasticIn等弹性曲线)。
    this.lowerBound = 0.0,  
    this.upperBound = 1.0,
    this.animationBehavior = AnimationBehavior.normal,
/*
  vsync接收一个TickerProvider类型的对象,它的主要职责是创建Ticker,定义如下:
    abstract class TickerProvider {
      // 通过一个回调创建一个Ticker
      Ticker createTicker(TickerCallback onTick);
    }
  通常会通过with将SingleTickerProviderStateMixin添加到State,然后将State对象this作为vsync的值。

  Flutter应用在启动时都会绑定一个SchedulerBinding,通过SchedulerBinding可以给每一次屏幕刷新添加回调,而Ticker就是通过SchedulerBinding来添加屏幕刷新回调,这样一来,每次屏幕刷新都会调用TickerCallback。
  使用Ticker(而不是Timer)来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源,因为Flutter中屏幕刷新时会通知到绑定的SchedulerBinding,而Ticker是受SchedulerBinding驱动的,由于锁屏后屏幕会停止刷新,所以Ticker就不会再触发。
*/
    required TickerProvider vsync,
  })

示例

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 2000), vsync: this);
//
final AnimationController controller = new AnimationController( 
 duration: const Duration(milliseconds: 2000), 
 lowerBound: 10.0,
 upperBound: 20.0,
 vsync: this
);
  1. Tween (继承自Animatable<T>,而不是Animation<T>)

通过begin和end定义生成值的区间(本身不存储任何状态)。

Animatable(定义动画值的映射规则)
  有一个animate方法,接收 Animation<double>类参数(通常是AnimationController),并返回一个Animation对象。
  animate方法定义如下:
    Animation<T> animate(Animation<double> parent) {
      return _AnimatedEvaluation<T>(parent, this);
    }

Tween对象提供了evaluate(Animation<double> animation)方法获取动画当前映射值。

示例

// Tween生成[-200.0,0.0]的值
final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);

// ColorTween将动画输入范围映射为两种颜色值之间过渡输出
final Tween colorTween =
    new ColorTween(begin: Colors.transparent, end: Colors.black54);
// 获取动画的当前值。多个Tween可共用一个controller。
doubleTween.evaluate(controller)
colorTween.evaluate(controller)

// 在500毫秒内生成从0到255的整数值
final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);

// 控制器、曲线、Tween
final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
    new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);

线性插值lerp函数

动画某一帧的状态值可以根据动画的进度来算出,Flutter中给有可能会做动画的一些状态属性都定义了静态的 lerp 方法(线性插值)。
lerp 的计算一般遵循: 返回值 = a + (b - a) * t

// a 为起始颜色,b为终止颜色,t为当前动画的进度[0,1]
Color.lerp(a, b, t);
Size.lerp(a, b, t)
Rect.lerp(a, b, t)
Offset.lerp(a, b, t)
Decoration.lerp(a, b, t)
Tween.lerp(t) // 起始状态和终止状态在构建Tween时已经指定了

2. 用AnimatedWidget简化、用AnimatedBuilder重构、监听状态

示例(未使用AnimatedWidget)

class ScaleAnimationRoute extends StatefulWidget {
  @override
  _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}
// 如果有多个AnimationController则应该使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>  with SingleTickerProviderStateMixin{ 
  Animation<double> animation;
  AnimationController controller;
  initState() {
    super.initState();
    controller = new AnimationController(duration: const Duration(seconds: 3), vsync: this);
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
      ..addListener(() {
        // 每次动画生成一个新值时,当前帧被标记为脏(dirty),这会导致widget的build()方法再次被调用,而在build()中Image的宽高为animation.value会逐渐放大。
        setState(()=>{});
      });
    // 启动动画(正向执行)
    controller.forward();
  }
  @override
  Widget build(BuildContext context) {
    return new Center(
       child: Image.asset("imgs/avatar.png",
          width: animation.value,
          height: animation.value
      ),
    );
  }
  // 路由销毁时释放动画资源
  dispose() {
    controller.dispose();
    super.dispose();
  }
}
========================
上例中并没有指定Curve,所以放大的过程是线性的(匀速的)。下面指定Curve来实现类似弹簧效果,修改initState中的代码:
  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(seconds: 3), vsync: this);
    // 使用弹性曲线
    animation=CurvedAnimation(parent: controller, curve: Curves.bounceIn);
    animation = new Tween(begin: 0.0, end: 300.0).animate(animation)
      ..addListener(() {
        setState(() {
        });
      });
    controller.forward();
  }

如果通过addListener()和setState() 来更新UI,每个动画中都要加这么一句是比较繁琐的。

  1. AnimatedWidget(对动画进行了简化,并可复用)

封装了调用addListener()和setState()的细节,并允许将widget分离出。

// 继承自StatefulWidget
AnimatedWidget({
    Key? key,
    required this.listenable,  // Animation
  }) : assert(listenable != null),
       super(key: key);

看一下AnimatedWidget内部的_AnimatedState实现:
class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }
  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }
  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }
  void _handleChange() {
    setState(() {
    });
  }
}

使用AnimatedWidget对示例进行重构,代码如下

class AnimatedImage extends AnimatedWidget {
  AnimatedImage({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: Image.asset("imgs/avatar.png",
          width: animation.value,
          height: animation.value
      ),
    );
  }
}
class ScaleAnimationRoute1 extends StatefulWidget {
  @override
  _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;
  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(seconds: 3), vsync: this);
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    controller.forward();
  }
  @override
  Widget build(BuildContext context) {
    return AnimatedImage(animation: animation,);
  }
  dispose() {
    controller.dispose();
    super.dispose();
  }
}

示例(AnimatedWidget、3D旋转)

class ThreeDAnimatedWidget extends AnimatedWidget {
  final Widget child;
  const ThreeDAnimatedWidget(
      {Key? key, required Animation<double> animation, required this.child})
      : super(key: key, listenable: animation);
  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        transform: Matrix4.identity()
          ..rotateY(2 * pi * animation.value)
          ..setEntry(1, 0, 0.01), // 绕Y轴旋转,setEntry设置倾斜角
        transformAlignment: Alignment.center,  // 锚点
        child: child,
      ),
    );
  }
}
  1. 使用AnimatedBuilder重构(分离组件和动画效果,实现动画效果复用)

用AnimatedWidget可以从动画中分离出widget,而动画的渲染过程(即设置宽高)仍然在AnimatedWidget中,假设如果再添加一个widget透明度变化的动画,那么需要再实现一个AnimatedWidget,这样不是很优雅,如果能把渲染过程也抽象出来,那就会好很多。AnimatedBuilder正是将渲染逻辑分离出来。

// 只负责管理动画效果,不应该管理组件构建。
AnimatedBuilder({
    Key? key,
    required Listenable animation,
    // builder用于构建组件的转变动作,在builder里可以对要渲染的子组件进行转变操作,然后返回变换后的组件。
    // Widget Function(BuildContext context, Widget? child)
    required this.builder, 
    this.child,  // 
  }) : assert(animation != null),
       assert(builder != null),
       super(key: key, listenable: animation);

好处:
    1. 不用显式的去添加帧监听器,然后再调用setState() 了。这个好处和AnimatedWidget一样。
    2. 动画构建的范围缩小了,如果没有builder,setState()将会在父组件上下文中调用,这将会导致父组件的build方法重新调用;而有了builder之后,只会导致动画widget自身的build重新调用,避免不必要的rebuild。
    3. 通过AnimatedBuilder可以封装常见的过渡效果来复用动画。

使用AnimatedBuilder对实例进行重构,代码如下

@override
Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: animation,
      child: Image.asset("images/avatar.png"),
      builder: (BuildContext ctx, Widget child) {
        return new Center(
          child: Container(
              height: animation.value, 
              width: animation.value, 
              child: child,
          ),
        );
      },
    );
}

示例(对子widget实现放大动画,复用)

class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});
  final Widget child;
  final Animation<double> animation;
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
          animation: animation,
          builder: (BuildContext context, Widget child) {
            return new Container(
                height: animation.value, 
                width: animation.value, 
                child: child
            );
          },
          child: child
      ),
    );
  }
}

这样,最初的示例就可以改为:
Widget build(BuildContext context) {
    return GrowTransition(
      child: Image.asset("images/avatar.png"), 
      animation: animation,
    );
}
Flutter中正是通过这种方式封装了很多动画(预置的过渡类),如:FadeTransition、ScaleTransition、SizeTransition等。

示例2(Transform 组件1转换一半后更换为组件2继续转换)

class RotationSwitchAnimatedBuilder extends StatelessWidget {
  final Widget child1, child2;
  final Animation<double> animation;
  const RotationSwitchAnimatedBuilder(
      {Key? key,
      required this.animation,
      required this.child1,
      required this.child2})
      : super(key: key);
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        if (animation.value < 0.5) {
          return Transform(
            transform: Matrix4.identity()
              ..rotateZ(animation.value * pi)
              ..setEntry(0, 1, -0.003),
            alignment: Alignment.center,
            child: child1,
          );
        } else {
          return Transform(
            transform: Matrix4.identity()
              ..rotateZ(pi)
              ..rotateZ(animation.value * pi)
              ..setEntry(1, 0, 0.003),
            child: child2,
            alignment: Alignment.center,
          );
        }
      },
    );
  }
}
==================
使用
class AnimatedBuilderDemo extends StatefulWidget {
  const AnimatedBuilderDemo({Key? key}) : super(key: key);
  @override
  _AnimatedBuilderDemoState createState() => _AnimatedBuilderDemoState();
}
class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
    with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;
  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 1), vsync: this);
    animation = Tween<double>(begin: 0, end: 1.0).animate(controller);
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedBuilder 动画'),
      ),
      body: RotationSwitchAnimatedBuilder(
        animation: animation,
        child1: Center(
          child: Container(
            padding: EdgeInsets.all(10),
            margin: EdgeInsets.all(10),
            constraints: BoxConstraints(minWidth: double.infinity),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(4.0),
              gradient: LinearGradient(
                colors: [
                  Colors.orange,
                  Colors.green,
                ],
              ),
            ),
            child: Text(
              '点击按钮变出小姐姐',
              style: TextStyle(
                fontSize: 20,
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
              textAlign: TextAlign.center,
            ),
          ),
        ),
        child2: Center(
          child: Image.asset('images/beauty.jpeg'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow, color: Colors.white),
        onPressed: () {
          if (controller.status == AnimationStatus.completed) {
            controller.reverse();
          } else {
            controller.forward();
          }
        },
      ),
    );
  }
  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

3. 自定义路由切换动画

Material组件库中提供了一个MaterialPageRoute组件,它可以使用和平台风格一致的路由切换动画(在iOS上会左右滑动切换,而在Android上会上下滑动切换)。

如果在Android上也想使用左右切换风格:

1. 简单的作法是直接使用CupertinoPageRoute
 // CupertinoPageRoute是Cupertino组件库提供的iOS风格的路由切换组件,它实现的就是左右滑动切换。
 Navigator.push(context, CupertinoPageRoute(  
   builder: (context)=>PageB(),
 ));

2. 使用PageRouteBuilder来自定义路由切换动画。
  pageBuilder有一个animation参数,这是Flutter路由管理器提供的,在路由切换时pageBuilder在每个动画帧都会被回调,因此可以通过animation对象来自定义过渡动画。

无论是MaterialPageRoute、CupertinoPageRoute,还是PageRouteBuilder,它们都继承自PageRoute类,而PageRouteBuilder其实只是PageRoute的一个包装,可以直接继承PageRoute类来实现自定义路由。

例(PageRouteBuilder自定义路由切换动画,建议)

以渐隐渐入动画来实现路由过渡

Navigator.push(
  context,
  PageRouteBuilder(
    transitionDuration: Duration(milliseconds: 500), // 动画时间为500毫秒
    pageBuilder: (BuildContext context, Animation animation,
        Animation secondaryAnimation) {
      return new FadeTransition(
        // 使用渐隐渐入过渡,
        opacity: animation,
        child: PageB(), // 路由B
      );
    },
  ),
);

例(直接继承自PageRoute)

上面的例子也可以通过如下方式实现:

1. 定义一个路由类FadeRoute
    class FadeRoute extends PageRoute {
      FadeRoute({
        @required this.builder,
        this.transitionDuration = const Duration(milliseconds: 300),
        this.opaque = true,
        this.barrierDismissible = false,
        this.barrierColor,
        this.barrierLabel,
        this.maintainState = true,
      });
      final WidgetBuilder builder;
      @override
      final Duration transitionDuration;
      @override
      final bool opaque;
      @override
      final bool barrierDismissible;
      @override
      final Color barrierColor;
      @override
      final String barrierLabel;
      @override
      final bool maintainState;
      @override
      Widget buildPage(BuildContext context, Animation<double> animation,
          Animation<double> secondaryAnimation) => builder(context);
      @override
      Widget buildTransitions(BuildContext context, Animation<double> animation,
          Animation<double> secondaryAnimation, Widget child) {
         return FadeTransition( 
           opacity: animation,
           child: builder(context),
         );
      }
    }
2. 使用FadeRoute
    Navigator.push(context, FadeRoute(builder: (context) {
      return PageB();
    }));

优先考虑使用PageRouteBuilder,但是有些时候PageRouteBuilder是不能满足需求的,例如在应用过渡动画时需要读取当前路由的一些属性,这时就只能通过继承PageRoute的方式了。
例如只想在打开新路由时应用动画,而在返回时不使用动画,那么在构建过渡动画时就必须判断当前路由isActive属性是否为true,代码如下:

@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
 // 当前路由被激活,是打开新路由
 if(isActive) {
   return FadeTransition(
     opacity: animation,
     child: builder(context),
   );
 }else{
   // 是返回,则不应用过渡动画
   return Padding(padding: EdgeInsets.zero);
 }
}

4. Hero动画

把一个组件以动画的方式从一个页面‘传递’到下一个页面(实际上是两个不同的组件,存在关联,显示相同的内容,位置、外观可能不同),能够在两个页面之间建立视觉锚点链接,从而起到引导用户的作用。

Hero({
  Key? key,
  required this.tag,  // 同一个页面的多个Hero不能使用相同的tag
  this.createRectTween,  // RectTween类型,保证Hero组件动画前后能够达到矩形指定位置和大小。用来自定义路径。
  this.flightShuttleBuilder,  // 设置飞行过程中的组件,默认使用旧路由的共享元素(即Hero子组件)
  this.placeholderBuilder,
  this.transitionOnUserGestures = false,
  required this.child,
})

使用
  分别使用Hero组件将2个widget包裹起来,设置相同的tag。

原理
  Flutter框架根据新旧路由页中共享元素的位置和大小,在动画执行过程中算出过渡时的插值。创建了一个(界面为旧路由的共享元素)遮罩层,执行动画让遮罩层界面变为新路由的共享元素,最后销毁遮罩层。

示例(Hero动画)

假设有两个路由A和B,他们的内容交互如下:
  A:包含一个用户头像,圆形,点击后跳到B路由,可以查看大图。
  B:显示用户头像原图,矩形;
在AB两个路由之间跳转的时候,用户头像会逐渐过渡到目标路由页的头像上

// 路由A
class HeroAnimationRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.topCenter,
      child: InkWell(
        child: Hero(
          tag: "avatar", // 唯一标记,前后两个路由页Hero的tag必须相同
          child: ClipOval(
            child: Image.asset("images/avatar.png",
              width: 50.0,
            ),
          ),
        ),
        onTap: () {
          // 打开B路由  
          Navigator.push(context, PageRouteBuilder(
              pageBuilder: (BuildContext context, Animation animation,
                  Animation secondaryAnimation) {
                return new FadeTransition(
                  opacity: animation,
                  child: Scaffold(
                    appBar: AppBar(
                      title: Text("原图"),
                    ),
                    body: HeroAnimationRouteB(),
                  ),
                );
              })
          );
        },
      ),
    );
  }
}
// 路由B
class HeroAnimationRouteB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Hero(
          tag: "avatar", // 唯一标记,前后两个路由页Hero的tag必须相同
          child: Image.asset("images/avatar.png"),
      ),
    );
  }
}

RectTween (自定义路径)

官网说明图
看一下RectTween类:
// 每次动画时间点上调用Rect.lerp方法构建一个插值矩形。
class RectTween extends Tween<Rect?> {
  RectTween({ Rect? begin, Rect? end }) : super(begin: begin, end: end);
  @override
  Rect? lerp(double t) => Rect.lerp(begin, end, t); 
}

继续看一下Rect.lerp方法:
  static Rect? lerp(Rect? a, Rect? b, double t) {
    assert(t != null);
    if (b == null) {
      if (a == null) {
        return null;
      } else {
        final double k = 1.0 - t;
        return Rect.fromLTRB(a.left * k, a.top * k, a.right * k, a.bottom * k);
      }
    } else {
      if (a == null) {
        return Rect.fromLTRB(b.left * t, b.top * t, b.right * t, b.bottom * t);
      } else {  
        // 在矩形a和b都不为空时,返回一个通过顶点定义的新矩形。
        return Rect.fromLTRB(
          _lerpDouble(a.left, b.left, t),
          _lerpDouble(a.top, b.top, t),
          _lerpDouble(a.right, b.right, t),
          _lerpDouble(a.bottom, b.bottom, t),
        );
      }
    }
  }

继续看一下_lerpDouble 方法(根据动画时间完成顶点的移动):
double? lerpDouble(num? a, num? b, double t) {
  return a * (1.0 - t) + b * t;
}

示例(自定义RectTween)

class CustomRectTween extends RectTween {
  final Rect begin;
  final Rect end;
  CustomRectTween({required this.begin, required this.end})
      : super(begin: begin, end: end);
  @override
  Rect lerp(double t) {
    double transformT = Curves.easeInOutBack.transform(t);
    var rect = Rect.fromLTRB(
        _rectMove(begin.left, end.left, transformT),
        _rectMove(begin.top, end.top, transformT),
        _rectMove(end.right, end.right, transformT),
        _rectMove(begin.bottom, end.bottom, transformT));
    return rect;
  }
  double _rectMove(double begin, double end, double t) {
    return begin * (1 - t) + end * t;
  }
}

flightShuttleBuilder(飞行过程中的组件)

typedef HeroFlightShuttleBuilder = Widget Function(
  BuildContext flightContext,
  Animation<double> animation,
  HeroFlightDirection flightDirection,  // 枚举,push、pop
  BuildContext fromHeroContext,
  BuildContext toHeroContext,
);
注意平台兼容性

示例(flightShuttleBuilder)

// flightShuttleBuilder属性只用在旧路由的Hero组件中定义即可,无需在新路由的Hero组件中定义。
flightShuttleBuilder: (_, animation, direction, __, ___) {
  if (direction == HeroFlightDirection.push) {
    return ClipOval(
      child: Opacity(
        child: Container(color: Colors.pink),
        opacity: 0.4,
      ),
    );
  } else {
    return ClipOval(
      child: Opacity(
        child: Container(color: Colors.blue),
        opacity: 0.4,
      ),
    );
  }
},

5. 交织动画(Stagger Animation)

有些时候可能会需要一些复杂的动画,这些动画可能由一个动画序列或重叠的动画组成。比如:有一个柱状图,需要在高度增长的同时改变颜色,等到增长到最大高度后,需要在X轴上平移一段距离。可以发现上述场景在不同阶段包含了多种动画,要实现这种效果,使用交织动画会非常简单。

注意:
    1. 要创建交织动画,需要使用多个动画Animation对象。
    2. 一个AnimationController控制所有的动画对象。
    3. 给每一个动画对象指定时间间隔

所有动画都由同一个AnimationController驱动,无论动画需要持续多长时间,控制器的值必须在0.0到1.0之间,而每个动画的间隔也必须介于0.0和1.0之间。对于在间隔中设置动画的每个属性,需要分别创建一个Tween 用于指定该属性的开始值和结束值。也就是说0.0到1.0代表整个动画过程,可以给不同动画指定不同的起始点和终止点来决定它们的开始时间和终止时间。

示例

实现一个柱状图增长的动画:
    1. 开始时高度从0增长到300像素,同时颜色由绿色渐变为红色;这个过程占据整个动画时间的60%。
    2. 高度增长到300后,开始沿X轴向右平移100像素;这个过程占用整个动画时间的40%。
class StaggerAnimation extends StatelessWidget {
  StaggerAnimation({ Key key, this.controller }): super(key: key){
    // 高度动画
    height = Tween<double>(
      begin:.0 ,
      end: 300.0,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(
          0.0, 0.6, // 间隔,前60%的动画时间
          curve: Curves.ease,
        ),
      ),
    );
    color = ColorTween(
      begin:Colors.green ,
      end:Colors.red,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(
          0.0, 0.6,// 间隔,前60%的动画时间
          curve: Curves.ease,
        ),
      ),
    );
    padding = Tween<EdgeInsets>(
      begin:EdgeInsets.only(left: .0),
      end:EdgeInsets.only(left: 100.0),
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(
          0.6, 1.0, // 间隔,后40%的动画时间
          curve: Curves.ease,
        ),
      ),
    );
  }
  final Animation<double> controller;
  Animation<double> height;
  Animation<EdgeInsets> padding;
  Animation<Color> color;
  Widget _buildAnimation(BuildContext context, Widget child) {
    return Container(
      alignment: Alignment.bottomCenter,
      padding:padding.value ,
      child: Container(
        color: color.value,
        width: 50.0,
        height: height.value,
      ),
    );
  }
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      builder: _buildAnimation,
      animation: controller,
    );
  }
}
class StaggerRoute extends StatefulWidget {
  @override
  _StaggerRouteState createState() => _StaggerRouteState();
}
class _StaggerRouteState extends State<StaggerRoute> with TickerProviderStateMixin {
  AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        duration: const Duration(milliseconds: 2000),
        vsync: this
    );
  }
  Future<Null> _playAnimation() async {
    try {
      // 先正向执行动画
      await _controller.forward().orCancel;
      // 再反向执行动画
      await _controller.reverse().orCancel;
    } on TickerCanceled {
      // the animation got canceled, probably because we were disposed
    }
  }
  @override
  Widget build(BuildContext context) {
    return  GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: () {
        _playAnimation();
      },
      child: Center(
        child: Container(
          width: 300.0,
          height: 300.0,
          decoration: BoxDecoration(
            color: Colors.black.withOpacity(0.1),
            border: Border.all(
              color:  Colors.black.withOpacity(0.5),
            ),
          ),
          // 调用定义的交织动画Widget
          child: StaggerAnimation(
              controller: _controller
          ),
        ),
      ),
    );
  }
}

6. AnimatedSwitcher(动画切换组件)

切换UI元素(Tab切换、路由切换)时通常会指定一个动画,以使切换过程显得平滑。
AnimatedSwitcher组件可以同时对其新、旧子元素添加显示、隐藏动画。

AnimatedSwitcher({
  Key key,
  this.child,
  @required this.duration, // 新child显示动画时长
  this.reverseDuration,// 旧child隐藏的动画时长
  this.switchInCurve = Curves.linear, // 新child显示的动画曲线
  this.switchOutCurve = Curves.linear,// 旧child隐藏的动画曲线
  this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // 动画构建器
  this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, // 布局构建器
})

说明:
  1. 当child发生变化时(且类型或Key不同),旧child会执行隐藏动画,新child会执行执行显示动画。
  2. transitionBuilder决定了执行什么动画效果
    接受一个AnimatedSwitcherTransitionBuilder类型的builder,定义如下:
    typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation<double> animation);
    该builder在child切换时会分别对新、旧child绑定动画:
      对旧child,绑定的动画会反向执行(reverse)
      对新child,绑定的动画会正向指向(forward)
    默认值是AnimatedSwitcher.defaultTransitionBuilder(“渐隐”和“渐显”动画)定义如下 :
      Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
        return FadeTransition(
          opacity: animation,
          child: child,
        );
      }
  3. layoutBuilder 
    设置新组件在组件树中的布局,定义如下:
    typedef AnimatedSwitcherLayoutBuilder = Widget Function(Widget? currentChild, List<Widget> previousChildren);
    默认值是AnimatedSwitcher.defaultLayoutBuilder(将当前组件放置在最顶层),定义如下
      static Widget defaultLayoutBuilder(Widget? currentChild, List<Widget> previousChildren) {
        return Stack(
          children: <Widget>[
            ...previousChildren,
            if (currentChild != null) currentChild,
          ],
          alignment: Alignment.center,
        );
      }

示例

实现一个计数器,然后再每一次自增的过程中,旧数字执行缩小动画隐藏,新数字执行放大动画显示

import 'package:flutter/material.dart';
class AnimatedSwitcherCounterRoute extends StatefulWidget {
   const AnimatedSwitcherCounterRoute({Key key}) : super(key: key);
   @override
   _AnimatedSwitcherCounterRouteState createState() => _AnimatedSwitcherCounterRouteState();
 }
 class _AnimatedSwitcherCounterRouteState extends State<AnimatedSwitcherCounterRoute> {
   int _count = 0;
   @override
   Widget build(BuildContext context) {
     return Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           AnimatedSwitcher(
             duration: const Duration(milliseconds: 500),
             transitionBuilder: (Widget child, Animation<double> animation) {
               // SizeTransition(sizeFactor: animation,child: child,)
               return ScaleTransition(child: child, scale: animation);
             },
             child: Text(
               '$_count',
               // 显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
               key: ValueKey<int>(_count),
               style: Theme.of(context).textTheme.headline4,
             ),
           ),
           RaisedButton(
             child: const Text('+1',),
             onPressed: () {
               setState(() {
                 _count += 1;
               });
             },
           ),
         ],
       ),
     );
   }
 }
当点击“+1”按钮时,原先的数字会逐渐缩小直至隐藏,而新数字会逐渐放大

AnimatedSwitcher实现原理

要想实现新旧child切换动画,只需要明确两个问题:动画执行的时机是和如何对新旧child执行动画。
从AnimatedSwitcher的使用方式可以看到,当child发生变化时(子widget的key和类型不同时相等则认为发生变化),则重新会重新执行build,然后动画开始执行。

可以通过继承StatefulWidget来实现AnimatedSwitcher,具体做法是在didUpdateWidget 回调中判断其新旧child是否发生变化,如果发生变化,则对旧child执行反向退场(reverse)动画,对新child执行正向(forward)入场动画。

下面是AnimatedSwitcher实现的部分核心伪代码:
Widget _widget; //
void didUpdateWidget(AnimatedSwitcher oldWidget) {
  super.didUpdateWidget(oldWidget);
  // 检查新旧child是否发生变化(key和类型同时相等则返回true,认为没变化)
  if (Widget.canUpdate(widget.child, oldWidget.child)) {
    // child没变化,...
  } else {
    //child发生了变化,构建一个Stack来分别给新旧child执行动画
   _widget= Stack(
      alignment: Alignment.center,
      children:[
        //旧child应用FadeTransition
        FadeTransition(
         opacity: _controllerOldAnimation,
         child : oldWidget.child,
        ),
        //新child应用FadeTransition
        FadeTransition(
         opacity: _controllerNewAnimation,
         child : widget.child,
        ),
      ]
    );
    // 给旧child执行反向退场动画
    _controllerOldAnimation.reverse();
    //给新child执行正向入场动画
    _controllerNewAnimation.forward();
  }
}
//build方法
Widget build(BuildContext context){
  return _widget;
}
AnimatedSwitcher的真正实现比这个复杂,它可以自定义进退场过渡动画以及执行动画时的布局等。

Flutter SDK中还提供了一个AnimatedCrossFade组件,它也可以切换两个子元素,切换过程执行渐隐渐显的动画,和AnimatedSwitcher不同的是AnimatedCrossFade是针对两个子元素,而AnimatedSwitcher是在一个子元素的新旧值之间切换。AnimatedCrossFade实现原理比较简单,也有和AnimatedSwitcher类似的地方.
 AnimatedCrossFade({
  Key? key,
  required this.firstChild,
  required this.secondChild,
  this.firstCurve = Curves.linear,
  this.secondCurve = Curves.linear,
  this.sizeCurve = Curves.linear,
  this.alignment = Alignment.topCenter,
  required this.crossFadeState,
  required this.duration,
  this.reverseDuration,
  this.layoutBuilder = defaultLayoutBuilder,
})
例:
AnimatedCrossFade(
  duration: const Duration(seconds: 3),
  firstChild: const FlutterLogo(style: FlutterLogoStyle.horizontal, size: 100.0),
  secondChild: const FlutterLogo(style: FlutterLogoStyle.stacked, size: 100.0),
  crossFadeState: _first ? CrossFadeState.showFirst : CrossFadeState.showSecond,
)

AnimatedSwitcher高级用法

假设现在想实现一个类似路由平移切换的动画:旧页面屏幕中向左侧平移退出,新页面重屏幕右侧平移进入。如果要用AnimatedSwitcher的话,很快就会发现一个问题:做不到。我们可能会写出下面的代码:
AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
     return SlideTransition(
       child: child,
       position: tween.animate(animation),
    );
  },
  ...//省略
)
AnimatedSwitcher的child切换时会分别对新child执行正向动画(forward),而对旧child执行反向动画(reverse),所以真正的效果便是:新child确实从屏幕右侧平移进入了,但旧child却会从屏幕右侧(而不是左侧)退出。因为同一个Animation正向(forward)和反向(reverse)是对称的。

所以如果可以打破这种对称性,那么便可以实现这个功能了,下面来封装一个MySlideTransition,它与SlideTransition唯一的不同就是对动画的反向执行进行了定制(从左边滑出隐藏),代码如下:
class MySlideTransition extends AnimatedWidget {
  MySlideTransition({
    Key key,
    @required Animation<Offset> position,
    this.transformHitTests = true,
    this.child,
  })
      : assert(position != null),
        super(key: key, listenable: position) ;

  Animation<Offset> get position => listenable;
  final bool transformHitTests;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    Offset offset=position.value;
    // 动画反向执行时,调整x偏移,实现“从左边滑出隐藏”
    if (position.status == AnimationStatus.reverse) {
         offset = Offset(-offset.dx, offset.dy);
    }
    return FractionalTranslation(
      translation: offset,
      transformHitTests: transformHitTests,
      child: child,
    );
  }
}
调用时,将SlideTransition替换成MySlideTransition即可:
AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
     return MySlideTransition(
              child: child,
              position: tween.animate(animation),
              );
  },
  ...//省略
)

SlideTransitionX(一个自定义组件)

上面的示例实现了“左出右入”的动画,那如果要实现“左入右出”、“上入下出”或者 “下入上出”怎么办?当然,可以分别修改上面的代码,但是这样每种动画都得单独定义一个“Transition”,这很麻烦。
class SlideTransitionX extends AnimatedWidget {
  SlideTransitionX({
    Key key,
    @required Animation<double> position,
    this.transformHitTests = true,
    this.direction = AxisDirection.down,
    this.child,
  })
      : assert(position != null),
        super(key: key, listenable: position) {
    // 偏移在内部处理      
    switch (direction) {
      case AxisDirection.up:
        _tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
        break;
      case AxisDirection.right:
        _tween = Tween(begin: Offset(-1, 0), end: Offset(0, 0));
        break;
      case AxisDirection.down:
        _tween = Tween(begin: Offset(0, -1), end: Offset(0, 0));
        break;
      case AxisDirection.left:
        _tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));
        break;
    }
  }
  Animation<double> get position => listenable;
  final bool transformHitTests;
  final Widget child;
  //退场(出)方向
  final AxisDirection direction;
  Tween<Offset> _tween;
  @override
  Widget build(BuildContext context) {
    Offset offset = _tween.evaluate(position);
    if (position.status == AnimationStatus.reverse) {
      switch (direction) {
        case AxisDirection.up:
          offset = Offset(offset.dx, -offset.dy);
          break;
        case AxisDirection.right:
          offset = Offset(-offset.dx, offset.dy);
          break;
        case AxisDirection.down:
          offset = Offset(offset.dx, -offset.dy);
          break;
        case AxisDirection.left:
          offset = Offset(-offset.dx, offset.dy);
          break;
      }
    }
    return FractionalTranslation(
      translation: offset,
      transformHitTests: transformHitTests,
      child: child,
    );
  }
}
如果想实现各种“滑动出入动画”便非常容易,只需给direction传递不同的方向值即可,比如要实现“上入下出”:
AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
     return SlideTransitionX(
              child: child,
                       direction: AxisDirection.down, //上入下出
              position: animation,
              );
  },
  ...//省略其余代码
)

7. 动画过渡组件

将在Widget属性发生变化时会执行过渡动画的组件统称为”动画过渡组件“,而动画过渡组件最明显的一个特征就是它会在内部自管理AnimationController。

为了方便使用者可以自定义动画的曲线、执行时长、方向等,通常都需要使用者自己提供一个AnimationController对象来自定义这些属性值。但是,如此一来,使用者就必须得手动管理AnimationController,这又会增加使用的复杂性。因此,如果也能将AnimationController进行封装,则会大大提高动画组件的易用性。

  1. 自定义动画过渡组件

示例

实现一个AnimatedDecoratedBox,在decoration属性发生变化时,从旧状态变成新状态的过程可以执行一个过渡动画。

class AnimatedDecoratedBox1 extends StatefulWidget {
  AnimatedDecoratedBox1({
    Key key,
    @required this.decoration,
    this.child,
    this.curve = Curves.linear,
    @required this.duration,
    this.reverseDuration,
  });
  final BoxDecoration decoration;
  final Widget child;
  final Duration duration;
  final Curve curve;
  final Duration reverseDuration;
  @override
  _AnimatedDecoratedBox1State createState() => _AnimatedDecoratedBox1State();
}
class _AnimatedDecoratedBox1State extends State<AnimatedDecoratedBox1>
    with SingleTickerProviderStateMixin {
  @protected
  AnimationController get controller => _controller;
  AnimationController _controller;
  Animation<double> get animation => _animation;
  Animation<double> _animation;
  DecorationTween _tween;
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child){
        return DecoratedBox(
          decoration: _tween.animate(_animation).value,
          child: child,
        );
      },
      child: widget.child,
    );
  }
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: widget.duration,
      reverseDuration: widget.reverseDuration,
      vsync: this,
    );
    _tween = DecorationTween(begin: widget.decoration);
    _updateCurve();
  }
  void _updateCurve() {
    if (widget.curve != null)
      _animation = CurvedAnimation(parent: _controller, curve: widget.curve);
    else
      _animation = _controller;
  }
  @override
  void didUpdateWidget(AnimatedDecoratedBox1 oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.curve != oldWidget.curve)
      _updateCurve();
    _controller.duration = widget.duration;
    _controller.reverseDuration = widget.reverseDuration;
    if(widget.decoration!= (_tween.end ?? _tween.begin)){
      _tween
        ..begin = _tween.evaluate(_animation)
        ..end = widget.decoration;
      _controller
        ..value = 0.0
        ..forward();
    }
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

使用AnimatedDecoratedBox1来实现按钮点击后背景色从蓝色过渡到红色的效果:

Color _decorationColor = Colors.blue;
var duration = Duration(seconds: 1);
...//省略无关代码
AnimatedDecoratedBox(
  duration: duration,
  decoration: BoxDecoration(color: _decorationColor),
  child: FlatButton(
    onPressed: () {
      setState(() {
        _decorationColor = Colors.red;
      });
    },
    child: Text(
      "AnimatedDecoratedBox",
      style: TextStyle(color: Colors.white),
    ),
  ),
)
击后,按钮背景色会从蓝色向红色过渡

上面的代码虽然实现了期望功能,但是代码却比较复杂。AnimationController的管理以及Tween更新部分的代码都是可以抽象出来的,如果这些通用逻辑封装成基类,那么要实现动画过渡组件只需要继承这些基类,然后定制自身不同的代码(比如动画每一帧的构建方法)即可,这样将会简化代码。

为了方便开发者来实现动画过渡组件的封装,Flutter提供了一个ImplicitlyAnimatedWidget抽象类,它继承自StatefulWidget,同时提供了一个对应的ImplicitlyAnimatedWidgetState类,AnimationController的管理就在ImplicitlyAnimatedWidgetState类中。
开发者如果要封装动画,只需要分别继承ImplicitlyAnimatedWidget和ImplicitlyAnimatedWidgetState类即可。

分两步实现

1. 继承ImplicitlyAnimatedWidget类
class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
  AnimatedDecoratedBox({
    Key key,
    @required this.decoration,
    this.child,
    Curve curve = Curves.linear, // 动画曲线
    @required Duration duration, // 正向动画执行时长
    Duration reverseDuration, // 反向动画执行时长
  }) : super(
          key: key,
          curve: curve,
          duration: duration,
          reverseDuration: reverseDuration,
        );
  final BoxDecoration decoration;
  final Widget child;
  @override
  _AnimatedDecoratedBoxState createState() {
    return _AnimatedDecoratedBoxState();
  }
}

2. State类继承自AnimatedWidgetBaseState(该类继承自ImplicitlyAnimatedWidgetState类)。
class _AnimatedDecoratedBoxState
    extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
  DecorationTween _decoration; //定义一个Tween
  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: _decoration.evaluate(animation),
      child: widget.child,
    );
  }
  @override
  void forEachTween(visitor) {
    // 在需要更新Tween时,基类会调用此方法
    _decoration = visitor(_decoration, widget.decoration,
        (value) => DecorationTween(begin: value));
  }
}

可以看到我们实现了build和forEachTween两个方法。在动画执行过程中,每一帧都会调用build方法(调用逻辑在ImplicitlyAnimatedWidgetState中),所以在build方法中我们需要构建每一帧的DecoratedBox状态,因此得算出每一帧的decoration 状态,这个我们可以通过_decoration.evaluate(animation) 来算出,其中animation是ImplicitlyAnimatedWidgetState基类中定义的对象,_decoration是我们自定义的一个DecorationTween类型的对象,那么现在的问题就是它是在什么时候被赋值的呢?要回答这个问题,我们就得搞清楚什么时候需要对_decoration赋值。我们知道_decoration是一个Tween,而Tween的主要职责就是定义动画的起始状态(begin)和终止状态(end)。对于AnimatedDecoratedBox来说,decoration的终止状态就是用户传给它的值,而起始状态是不确定的,有以下两种情况:
    1. AnimatedDecoratedBox首次build,此时直接将其decoration值置为起始状态,即_decoration值为DecorationTween(begin: decoration) 。
    2. AnimatedDecoratedBox的decoration更新时,则起始状态为_decoration.animate(animation),即_decoration值为DecorationTween(begin: _decoration.animate(animation),end:decoration)。

现在forEachTween的作用就很明显了,它正是用于来更新Tween的初始值的,在上述两种情况下会被调用,而开发者只需重写此方法,并在此方法中更新Tween的起始状态值即可。而一些更新的逻辑被屏蔽在了visitor回调,我们只需要调用它并给它传递正确的参数即可,visitor方法签名如下:
   Tween visitor(
     Tween<dynamic> tween, //当前的tween,第一次调用为null
     dynamic targetValue, // 终止状态
     TweenConstructor<dynamic> constructor,//Tween构造器,在上述三种情况下会被调用以更新tween
   );

动画过渡组件的反向动画

在使用动画过渡组件,只需要在改变一些属性值后重新build组件即可,所以要实现状态反向过渡,只需要将前后状态值互换即可实现。

另一种方式(尽量避免使用):
ImplicitlyAnimatedWidget构造函数中有一个reverseDuration属性用于设置反向动画的执行时长。如果要让reverseDuration生效,只能先获取controller,然后再通过controller.reverse()来启动反向动画。

在上面示例的基础上实现一个循环的点击背景颜色变换效果,要求从蓝色变为红色时动画执行时间为400ms,从红变蓝为2s,如果要使reverseDuration生效需要这么做:
AnimatedDecoratedBox(
  duration: Duration( milliseconds: 400),
  decoration: BoxDecoration(color: _decorationColor),
  reverseDuration: Duration(seconds: 2),
  child: Builder(builder: (context) {
    return FlatButton(
      onPressed: () {
        if (_decorationColor == Colors.red) {
          ImplicitlyAnimatedWidgetState _state =
              context.findAncestorStateOfType<ImplicitlyAnimatedWidgetState>();
           // 通过controller来启动反向动画
          _state.controller.reverse().then((e) {
            // 经验证必须调用setState来触发rebuild,否则状态同步会有问题
            setState(() {
              _decorationColor = Colors.blue;
            });
          });
        } else {
          setState(() {
            _decorationColor = Colors.red;
          });
        }
      },
      child: Text(
        "AnimatedDecoratedBox toggle",
        style: TextStyle(color: Colors.white),
      ),
    );
  }),
)

上面的代码实际上是非常糟糕且没必要的,它需要了解ImplicitlyAnimatedWidgetState内部实现,并且要手动去启动反向动画。
完全可以通过如下代码实现相同的效果:
AnimatedDecoratedBox(
  duration: Duration(
      milliseconds: _decorationColor == Colors.red ? 400 : 2000),
  decoration: BoxDecoration(color: _decorationColor),
  child: Builder(builder: (context) {
    return FlatButton(
      onPressed: () {
        setState(() {
          _decorationColor = _decorationColor == Colors.blue
              ? Colors.red
              : Colors.blue;
        });
      },
      child: Text(
        "AnimatedDecoratedBox toggle",
        style: TextStyle(color: Colors.white),
      ),
    );
  }),
)

为什么ImplicitlyAnimatedWidgetState要提供一个reverseDuration参数呢?该参数并非是给ImplicitlyAnimatedWidgetState用的,而是给子类用的!
要使reverseDuration 有用就必须得获取controller 属性来手动启动反向动画,ImplicitlyAnimatedWidgetState中的controller 属性是一个保护属性,定义如下:
 @protected
  AnimationController get controller => _controller;
而保护属性原则上只应该在子类中使用,而不应该像上面示例代码一样在外部使用。
可以得出两条结论:
    1. 使用动画过渡组件时如果需要执行反向动画的场景,应尽量使用状态互换的方法,而不应该通过获取ImplicitlyAnimatedWidgetState中controller的方式。
    2. 如果自定义的动画过渡组件用不到reverseDuration ,那么最好就不要暴露此参数,比如上面自定义的AnimatedDecoratedBox定义中就可以去除reverseDuration 可选参数,如:
    class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
      AnimatedDecoratedBox({
        Key key,
        @required this.decoration,
        this.child,
        Curve curve = Curves.linear,
        @required Duration duration,
      }) : super(
              key: key,
              curve: curve,
              duration: duration,
            );

交错动画(组合动效)

实现要点:
  1. 所有的Anmation对象共用一个AnimationController(其值必须在0-1之间)依次驱动。
  2. 每个动画对象自己又有一个0-1范围内的Interval间隔(在该间隔内Tween对象从起始值过渡到结束值)。

Interval 类继承自 Curve,不同的是,在 begin 之前曲线的值一直保持为0.0,而在 end 之后一直保持为1.0。

示例

import 'package:flutter/material.dart';
class StaggeredAnimationDemo extends StatefulWidget {
  StaggeredAnimationDemo({Key? key}) : super(key: key);
  @override
  _StaggeredAnimationDemoState createState() => _StaggeredAnimationDemoState();
}
class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _opacity;
  late Animation<double> _width;
  late Animation<double> _height;
  late Animation<Color?> _color;
  @override
  void initState() {
    _controller =
        AnimationController(duration: Duration(seconds: 2), vsync: this)
          ..addListener(() {
            setState(() {});
          });
    _opacity = Tween<double>(begin: 0.5, end: 1.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(
          0.0,
          0.25,
          curve: Curves.easeIn,
        ),
      ),
    );
    _width = Tween<double>(begin: 0.0, end: 2.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(
          0.25,
          0.5,
          curve: Curves.easeIn,
        ),
      ),
    );
    _height = Tween<double>(begin: 0.0, end: 2.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(
          0.5,
          0.75,
          curve: Curves.easeIn,
        ),
      ),
    );
    _color = ColorTween(begin: Colors.green, end: Colors.blue).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(
          0.75,
          1.0,
          curve: Curves.easeIn,
        ),
      ),
    );
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('交错动画'),
      ),
      body: Center(
        child: Opacity(
          opacity: _opacity.value,
          child: Container(
            width: 100 + 100 * _width.value,
            height: 100 + 100 * _height.value,
            color: _color.value,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          if (_controller.isCompleted) {
              _controller.reverse();
            } else if (!_controller.isAnimating) {
              _controller.forward();
          }
        },
      ),
    );
  }
}

示例2(滚动的轮子)

// 轮子
class Wheel extends StatelessWidget {
  final double size;
  final Color color;
  final double time;
  const Wheel({
    Key? key,
    required this.size,
    required this.time,
    required this.color,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container(
      width: size,
      height: size,
      transform: Matrix4.identity()..rotateZ(2 * pi * time),
      transformAlignment: Alignment.center,
      decoration: BoxDecoration(
        border: Border.all(color: color, width: 10.0),
        borderRadius: BorderRadius.circular(size / 2),
        gradient: LinearGradient(
          colors: [
            Colors.white,
            Colors.orange[100]!,
            Colors.orange[400]!,
          ],
        ),
      ),
    );
  }
}
// UI构建
Widget build(BuildContext context) {
  final bottomHeight = MediaQuery.of(context).size.height / 3;
  return Scaffold(
    appBar: AppBar(
      title: const Text('交错动画'),
    ),
    body: Stack(children: [
      Positioned(
        child: Container(
          width: double.infinity,
          height: bottomHeight,
          color: Colors.green[400],
        ),
        bottom: 0,
        left: 0,
        right: 0,
      ),
      Positioned(
          child: Wheel(
            size: wheelSize,
            color: _color.value!,
            time: _time.value,
          ),
          left: _offset.value * MediaQuery.of(context).size.width,
          bottom: bottomHeight),
      Positioned(
          child: Wheel(
            size: wheelSize,
            color: _color.value!,
            time: -_time.value,
          ),
          right: _offset.value * MediaQuery.of(context).size.width,
          bottom: bottomHeight)
    ]),
    floatingActionButton: FloatingActionButton(
      child: Icon(Icons.play_arrow),
      onPressed: () {
        if (_controller.isCompleted) {
          _controller.reverse();
        } else if (!_controller.isAnimating) {
          _controller.forward();
        }
      },
    ),
  );
}
// 动画逻辑
late AnimationController _controller;
late Animation<double> _time;
late Animation<double> _offset;
late Animation<Color?> _color;
final wheelSize = 80.0;
@override
void initState() {
  _controller =
      AnimationController(duration: Duration(seconds: 4), vsync: this)
        ..addListener(() {
          setState(() {});
        });
  _time = Tween<double>(begin: 0, end: 8.0).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Interval(
        0.0,
        1.0,
        curve: Curves.linear,
      ),
    ),
  );
  _offset = Tween<double>(begin: 0, end: 1.0).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Interval(
        0.0,
        1.0,
        curve: Curves.easeInCubic,
      ),
    ),
  );
  _color = ColorTween(begin: Colors.black87, end: Colors.green).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Interval(
        0.0,
        0.8,
        curve: Curves.easeIn,
      ),
    ),
  );
  super.initState();
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 1. 基本动画概念和相关类 Animation: flutter 动画库中的一个核心类,它生成指导动画的值; An...
    IAMCJ阅读 1,331评论 0 8
  • 动画与打包 动画 ​ Flutter中的动画系统基于Animation对象的,和之前的手势不同,它不是...
    zcwfeng阅读 441评论 0 1
  • 1. 一个简单例子 我们通过一个下面一个简单的放大动画来了解动画中相关的Api。 从上述代码中,我们知道要实现一个...
    谢尔顿阅读 525评论 0 2
  • 邂逅FLutter 万物皆是Widget 一般缩进2个空格 文字居中 Widget Center() Materi...
    JackLeeVip阅读 3,118评论 0 4
  • 黑色的海岛上悬着一轮又大又圆的明月,毫不嫌弃地把温柔的月色照在这寸草不生的小岛上。一个少年白衣白发,悠闲自如地倚坐...
    小水Vivian阅读 3,093评论 1 5