# Flutter 动画

返回:flutter

TIP

动画系统是任何一个 UI 框架的核心功能,也是开发者学习一个 UI 框架的重中之重,同时也是比较难掌握的一部分

视觉暂留

任何程序的动画原理都是一样的,即:视觉暂留,视觉暂留又叫视觉暂停,人眼在观察景物时,光信号传入大脑神经,需经过一段短暂的时间,光的作用结束后,视觉形象并不立即消失,这种残留的视觉称“后像”,视觉的这一现象则被称为“视觉暂留”。

人眼能保留 0.1-0.4 秒左右的图像,所以在 1 秒内看到连续的 25 张图像,人就会感到画面流畅,而 1 秒内看到连续的多少张图像称为 帧率,即 FPS,理论上 达到 24 FPS 画面比较流畅,而 Flutter,理论上可以达到 60 FPS。

# 动画核心-AnimationController

它是动画控制器,控制动画的启动、停止,还可以获取动画的运行状态,AnimationController 通常在 initState 方法中初始化

  • vsync:当创建 AnimationController 时,需要传递一个 vsync 参数,存在 vsync 时会防止屏幕外动画消耗不必要的资源,单个 AnimationController 的时候使用 SingleTickerProviderStateMixin,多个 AnimationController 使用 TickerProviderStateMixin
  • duration:表示动画执行的时间。

在 State dispose 生命周期中释放 AnimationController

late AnimationController _animationController;


void initState() {
super.initState();
_animationController = AnimationController(
    vsync: this, duration: const Duration(milliseconds: 600))
    ..addListener(() {
    setState(() {
        // AnimationController 的值默认是 0 到 1
        _width = 120 + 120 * _animationController.value;
    });
    });
}


void dispose() {
super.dispose();
_animationController.dispose();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

其中最重要的就是 AnimationController,_animationController.value 是当前动画的值,默认从 0 到 1

也可以通过参数形式设置最大值和最小值

_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500),lowerBound: 100,upperBound: 200)
..addListener(() {
  setState(() {
    _size = _controller.value;
  });
})
1
2
3
4
5
6

此时 _controller.value 的值就是从 100 变化到 200。

除了使用 addListener 监听每一帧,还可以监听动画状态的变化:

_controller = AnimationController(
    vsync: this,
    duration: Duration(milliseconds: 500),
    lowerBound: 100,
    upperBound: 200)
  ..addStatusListener((status) {
    print('status:$status');
  })
1
2
3
4
5
6
7
8

# 动画的状态分为四种

  • dismissed:动画停止在开始处。
  • forward:动画正在从开始处运行到结束处(正向运行)。
  • reverse:动画正在从结束处运行到开始处(反向运行)。
  • completed:动画停止在结束处。

# 再来看下动画的控制方法

  • forward:正向执行动画。
  • reverse:反向执行动画。
  • repeat:反复执行动画。
  • reset:重置动画。

forward 和 reverse 方法中都有 from 参数,这个参数的意义是一样的,表示动画从此值开始执行,而不再是从 lowerBound 到 upperBound。比如上面的例子中 from 参数设置 150,那么执行动画时,蓝色盒子瞬间变为 150,然后再慢慢变大到 200。

让蓝色盒子大小从 100 到 200,然后再变到 100,再到 200,如此反复

# Tween

AnimationController 设置的最小/大值类型是 double,如果动画的变化是颜色要如何处理?

AnimationController 在执行动画期间返回的值是 0 到 1,颜色从蓝色变为红色方法如下

Flutter 中把这种从 0 -> 1 转换为 蓝色 -> 红色 行为称之为 Tween(映射)。

class _TweenDemoState extends State<TweenDemo>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<Color> _animation;

  
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: Duration(milliseconds: 500))
          ..addListener(() {
            setState(() {});
          });
    _animation =
        ColorTween(begin: Colors.blue, end: Colors.red).animate(_controller);
  }

  
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        onTap: () {
          _controller.forward();
        },
        child: Container(
          height: 100,
          width: 100,
          color: _animation.value,
          alignment: Alignment.center,
          child: Text(
            '点我变色',
            style: TextStyle(color: Colors.white, fontSize: 18),
          ),
        ),
      ),
    );
  }

  
  void dispose() {
    super.dispose();
    _controller.dispose();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

Tween 仅仅是映射,动画的控制依然由 AnimationController 控制,因此需要 Tween.animate(_controller) 将控制器传递给 Tween。

# Curve

TIP

动画中还有一个重要的概念就是 Curve,即动画执行曲线。Curve 的作用和 Android 中的 Interpolator(差值器)是一样的,负责控制动画变化的速率,通俗地讲就是使动画的效果能够以匀速、加速、减速、抛物线等各种速率变化。

动画加上 Curve 后,AnimationController 的最小/大值必须是 [0,1]之间,

# 自定义动画曲线需要继承 Curve 重写 transformInternal 方法即可

class _StairsCurve extends Curve {

  
  double transformInternal(double t) {
    return t;
  }
}
1
2
3
4
5
6
7

直接返回 t 其实就是线性动画,即 Curves.linear,实现楼梯效果动画代码如下

class _StairsCurve extends Curve {
  //阶梯的数量
  final int num;
  double _perStairY;
  double _perStairX;

  _StairsCurve(this.num) {
    _perStairY = 1.0 / (num - 1);
    _perStairX = 1.0 / num;
  }

  
  double transformInternal(double t) {
    return _perStairY * (t / _perStairX).floor();
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_animation = Tween(begin: 100.0, end: 200.0)
    .chain(CurveTween(curve: _StairsCurve(5)))
    .animate(_controller);
1
2
3

# 总结

动画系统的核心是 AnimationController,而且是不可或缺的,动画中必须有 AnimationController,
而 Tween 和 Curve 则是对 AnimationController 的补充, Tween 实现了将 AnimationController [0,1]的值映射为其他类型的值,比如颜色、样式等,Curve 是 AnimationController 动画执行曲线,默认是线性运行。

# AnimationController 、 Tween 、Curve 进行关联的方式

AnimationController _controller;
Animation _animation;


void initState() {
  super.initState();
  _controller =
      AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
        ..addListener(() {
          setState(() {});
        });

  _animation = Tween(begin: 100.0, end: 200.0)
      .animate(_controller);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_animation = _controller.drive(Tween(begin: 100.0, end: 200.0));
1

# 加入 Curve

_animation = Tween(begin: 100.0, end: 200.0)
    .chain(CurveTween(curve: Curves.linear))
    .animate(_controller);
1
2
3
_animation = _controller
    .drive(CurveTween(curve: Curves.linear))
    .drive(Tween(begin: 100.0, end: 200.0));
1
2
3

# Flutter 中组合动画使用 Interval

Animation _sizeAnimation = Tween(begin: 100.0, end: 300.0).animate(CurvedAnimation(
    parent: _animationController, curve: Interval(0.5, 1.0)));
1
2

表示_sizeAnimation 动画从 0.5(一半)开始到结束,如果动画时长为 6 秒,_sizeAnimation 则从第 3 秒开始。

Interval 中 begin 和 end 参数值的范围是0.0到1.0

# 特殊场景

TIP

一个红色的盒子,动画时长为 6 秒,前 40%的时间大小从 100->200,然后保持 200 不变 20%的时间,最后 40%的时间大小从 200->300,这种效果通过 TweenSequence 实现

_animation = TweenSequence([
  TweenSequenceItem(
      tween: Tween(begin: 100.0, end: 200.0)
          .chain(CurveTween(curve: Curves.easeIn)),
      weight: 40),
  TweenSequenceItem(tween: ConstantTween<double>(200.0), weight: 20),
  TweenSequenceItem(tween: Tween(begin: 200.0, end: 300.0), weight: 40),
]).animate(_animationController);
1
2
3
4
5
6
7
8

weight 表示每一个 Tween 的权重。

# 其他封装动画组件

# 系统封装的类似上面的组件是 AnimatedWidget,此类是抽象类

class AnimationDemo extends StatefulWidget {
  
  State<StatefulWidget> createState() => _AnimationDemo();
}

class _AnimationDemo extends State<AnimationDemo>
    with SingleTickerProviderStateMixin {
  AnimationController _animationController;
  Animation _animation;

  
  void initState() {
    _animationController =
        AnimationController(duration: Duration(seconds: 2), vsync: this);

    _animation = Tween(begin: .5, end: .1).animate(_animationController);

    //开始动画
    _animationController.forward();
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _animation,
      child: Container(
        height: 200,
        width: 200,
        color: Colors.red,
      ),
    );
  }

  
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# ImplicitlyAnimatedWidget

AnimatedWidget 只是封装了 setState,系统是否有封装 AnimationController、Tween、Curve 且自动管理 AnimationController 的组件呢?有的,此组件就是 ImplicitlyAnimatedWidget,ImplicitlyAnimatedWidget 也是一个抽象类

class AnimatedWidgetDemo extends StatefulWidget {
  
  _AnimatedWidgetDemoState createState() => _AnimatedWidgetDemoState();
}

class _AnimatedWidgetDemoState extends State<AnimatedWidgetDemo> {
  double _opacity = 1.0;

  
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedOpacity(
        opacity: _opacity,
        duration: Duration(seconds: 2),
        child: GestureDetector(
          onTap: () {
            setState(() {
              _opacity = 0;
            });
          },
          child: Container(
            height: 60,
            width: 150,
            color: Colors.blue,
          ),
        ),
      ),
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 隐式动画组件

只需提供给组件动画开始、结束值,组件创建 AnimationController、Curve、Tween,执行动画,释放 AnimationController,我们称之为隐式动画组件,隐式动画组件有: AnimatedAlign、AnimatedContainer、AnimatedDefaultTextStyle、AnimatedOpacity、AnimatedPadding、AnimatedPhysicalModel、AnimatedPositioned、AnimatedPositionedDirectional、AnimatedTheme、SliverAnimatedOpacity、TweenAnimationBuilder、AnimatedContainer 等

# 显示动画组件

需要设置 AnimationController,控制动画的执行,使用显式动画可以完成任何隐式动画的效果,甚至功能更丰富一些,不过你需要管理该动画的 AnimationController 生命周期,AnimationController 并不是一个控件,所以需要将其放在 stateful 控件中。显示动画组件有:AlignTransition、AnimatedBuilder、AnimatedModalBarrier、DecoratedBoxTransition、DefaultTextStyleTransition、PositionedTransition、RelativePositionedTransition、RotationTransition、ScaleTransition、SizeTransition、SlideTransition 、FadeTransition 等

# 万能动画组件

显示动画组件和隐式动画组件中各有一个万能的组件,它们是 AnimatedBuilderTweenAnimationBuilder,当系统中不存在我们想要的动画组件时,可以使用这两个组件

class AnimatedBuilderDemo extends StatefulWidget {
  
  _AnimatedBuilderDemoState createState() => _AnimatedBuilderDemoState();
}

class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<Color> _colorAnimation;
  Animation<Size> _sizeAnimation;

  
  void initState() {
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 2));

    _colorAnimation =
        ColorTween(begin: Colors.blue, end: Colors.red).animate(_controller);
    _sizeAnimation =
        SizeTween(begin: Size(100.0, 50.0), end: Size(200.0, 100.0))
            .animate(_controller);

    _controller.forward();
    super.initState();
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, widget) {
          return Container(
            width: _sizeAnimation.value.width,
            height: _sizeAnimation.value.height,
            color: _colorAnimation.value,
          );
        },
      ),
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 是否内置动画组件使用

  • 判断你的动画组件是否一直重复,比如一直转圈的 loading 动画,如果是选择显式动画。
  • 判断你的动画组件是否需要多个组件联动,如果是选择显式动画。
  • 判断你的动画组件是否需要组合动画,如果是选择显式动画。
  • 如果上面三个条件都是否,就选择隐式动画组件,判断是否已经内置动画组件,如果没有,使用 TweenAnimationBuilder,有就直接使用内置动画组件。
  • 选择显式动画组件,判断是否已经内置动画组件,如果没有,使用 AnimatedBuilder,有就直接使用内置动画组件。

# AnimatedList

AnimatedList 提供了一种简单的方式使列表数据发生变化时加入过渡动画

  • itemBuilder 一个函数,列表的每一个索引会调用,这个函数有一个 animation 参数,可以设置成任何一个动画
  • initialItemCount item 的个数
  • scrollDirection 滚动方向,默认垂直
  • controller scroll 控制器
class AnimatedListDemo extends StatefulWidget {
  
  State<StatefulWidget> createState() => _AnimatedListDemo();
}

class _AnimatedListDemo extends State<AnimatedListDemo>
    with SingleTickerProviderStateMixin {
  List<int> _list = [];
  final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();

  void _addItem() {
    final int _index = _list.length;
    _list.insert(_index, _index);
    _listKey.currentState.insertItem(_index);
  }

  void _removeItem() {
    final int _index = _list.length - 1;
    var item = _list[_index].toString();
    _listKey.currentState.removeItem(
        _index, (context, animation) => _buildItem(item, animation));
    _list.removeAt(_index);

  }

  Widget _buildItem(String _item, Animation _animation) {
    return SlideTransition(
      position: _animation.drive(CurveTween(curve: Curves.easeIn)).drive(Tween<Offset>(begin: Offset(1,1),end: Offset(0,1))),
      child: Card(
        child: ListTile(
          title: Text(
            _item,
          ),
        ),
      ),
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnimatedList(
        key: _listKey,
        initialItemCount: _list.length,
        itemBuilder: (BuildContext context, int index, Animation animation) {
          return _buildItem(_list[index].toString(), animation);
        },
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          FloatingActionButton(
            onPressed: () => _addItem(),
            child: Icon(Icons.add),
          ),
          SizedBox(
            width: 60,
          ),
          FloatingActionButton(
            onPressed: () => _removeItem(),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# 路由动画

转场 就是从当前页面跳转到另一个页面,跳转页面在 Flutter 中通过 Navigator,跳转到新页面如下:

Navigator.push(context, MaterialPageRoute(builder: (context) {
  return _TwoPage();
}));
1
2
3

回退到前一个页面:

Navigator.pop(context);
1

Flutter 提供了两个转场动画,分别为 MaterialPageRoute 和 CupertinoPageRoute,

  • MaterialPageRoute 根据不同的平台显示不同的效果,Android 效果为从下到上,iOS 效果为从左到右。
  • CupertinoPageRoute 不分平台,都是从左到右。

通过源代码发现,默认情况下没有动画效果。

自定义转场动画只需修改 transitionsBuilder 即可:

Navigator.push(
    context,
    PageRouteBuilder(pageBuilder: (
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
    ) {
      return _TwoPage();
    }, transitionsBuilder: (BuildContext context,
        Animation<double> animation,
        Animation<double> secondaryAnimation,
        Widget child) {
      return SlideTransition(
        position: Tween(begin: Offset(-1, 0), end: Offset(0, 0))
            .animate(animation),
        child: child,
      );
    }));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

将其封装,方便使用:

class LeftToRightPageRoute extends PageRouteBuilder {
  final Widget newPage;

  LeftToRightPageRoute(this.newPage)
      : super(
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) =>
              newPage,
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) =>
              SlideTransition(
            position: Tween(begin: Offset(-1, 0), end: Offset(0, 0))
                .animate(animation),
            child: child,
          ),
        );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Navigator.push(context, LeftToRightPageRoute(_TwoPage()));
1

不仅是这些平移动画,前面所学的旋转、缩放等动画直接替换 SlideTransition 即可。

上面的动画只对新的页面进行了动画,如果想实现当前页面被新页面从顶部顶出的效果,实现方式如下:

class CustomPageRoute extends PageRouteBuilder {
  final Widget currentPage;
  final Widget newPage;

  CustomPageRoute(this.currentPage, this.newPage)
      : super(
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) =>
              currentPage,
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) =>
              Stack(
            children: <Widget>[
              SlideTransition(
                position: new Tween<Offset>(
                  begin: const Offset(0, 0),
                  end: const Offset(0, -1),
                ).animate(animation),
                child: currentPage,
              ),
              SlideTransition(
                position: new Tween<Offset>(
                  begin: const Offset(0, 1),
                  end: Offset(0, 0),
                ).animate(animation),
                child: newPage,
              )
            ],
          ),
        );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Navigator.push(context, CustomPageRoute(this, _TwoPage()));
1