老孟导读:动画系统是任何一个UI框架的核心功能,也是开发者学习一个UI框架的重中之重,同时也是比较难掌握的一部分,下面我们就一层一层的揭开 Flutter 动画的面纱。

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

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

AnimationController

介绍完了动画系统的基本原理,实现一个蓝色盒子大小从 100 变为 200动画效果:

class AnimationBaseDemo extends StatefulWidget {
@override
_AnimationBaseDemoState createState() => _AnimationBaseDemoState();
} class _AnimationBaseDemoState extends State<AnimationBaseDemo> {
double _size = 100; @override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
setState(() {
_size = 200;
});
},
child: Container(
height: _size,
width: _size,
color: Colors.blue,
alignment: Alignment.center,
child: Text('点我变大',style: TextStyle(color: Colors.white,fontSize: 18),),
),
),
);
}
}

虽然变大了,但并没有动画效果,而是直接变大的,想要使其一点点放大需要引入 AnimationController,它是动画控制器,控制动画的启动、停止,还可以获取动画的运行状态,AnimationController 通常在 initState 方法中初始化:

class _AnimationBaseDemoState extends State<AnimationBaseDemo> with SingleTickerProviderStateMixin{
double _size = 100;
AnimationController _controller; @override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500));
}
...
}

这里有两个参数需要设置:

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

修改如下:

class _AnimationBaseDemoState extends State<AnimationBaseDemo> with SingleTickerProviderStateMixin{
double _size = 100;
AnimationController _controller; @override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500));
} @override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
_controller.forward();
},
child: Container(
height: _size,
width: _size,
color: Colors.blue,
alignment: Alignment.center,
child: Text('点我变大',style: TextStyle(color: Colors.white,fontSize: 18),),
),
),
);
} @override
void dispose() {
super.dispose();
_controller.dispose();
}
}

点击蓝色盒子的时候不再直接更改大小,而是执行动画_controller.forward()

另外在State dispose 生命周期中释放 AnimationController。

此时点击蓝色盒子发现并不会变大,StatefulWidget 组件改变外观需要调用 setState,因此给 AnimationController 添加监听:

_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {
_size = 100+100*_controller.value;
});
});

每一帧都会回调addListener ,在此回调中设置蓝色盒子大小,蓝色的大小是由 100 变到 200,而 AnimationController 的值默认是 0 到 1,所以蓝色大小等于 _size = 100+100*_controller.value,运行效果:

这就是 Flutter 中最简单动画的实现方式,其中最重要的就是 AnimationController,_controller.value 是当前动画的值,默认从 0 到 1。也可以通过参数形式设置最大值和最小值:

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

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

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

_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
lowerBound: 100,
upperBound: 200)
..addStatusListener((status) {
print('status:$status');
})

动画的状态分为四种:

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

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

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

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

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

_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
lowerBound: 100,
upperBound: 200)
..addStatusListener((AnimationStatus status) {
if(status == AnimationStatus.completed){
_controller.reverse();
}else if(status == AnimationStatus.dismissed){
_controller.forward();
}
})
..addListener(() {
setState(() {
_size = _controller.value;
});
});

只需监听动画状态变化,在动画结束后再正向/反向再次执行动画。

虽然上面讲了很多,但只有一个重点 AnimationController

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

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

_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {
_color = Color.lerp(_startColor, _endColor, _controller.value);
});
});

重点是 Color.lerp 方法,此方法是在两种颜色之间线性插值。

完整代码如下:

class TweenDemo extends StatefulWidget {
@override
_TweenDemoState createState() => _TweenDemoState();
} class _TweenDemoState extends State<TweenDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Color _startColor = Colors.blue;
Color _endColor = Colors.red; Color _color = Colors.blue; @override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {
_color = Color.lerp(_startColor, _endColor, _controller.value);
});
});
} @override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
_controller.forward();
},
child: Container(
height: 100,
width: 100,
color: _color,
alignment: Alignment.center,
child: Text(
'点我变色',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
} @override
void dispose() {
super.dispose();
_controller.dispose();
}
}

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

使用 Tween 完成动画 蓝色 -> 红色:

class _TweenDemoState extends State<TweenDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<Color> _animation; @override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
_animation =
ColorTween(begin: Colors.blue, end: Colors.red).animate(_controller);
} @override
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),
),
),
),
);
} @override
void dispose() {
super.dispose();
_controller.dispose();
}
}

效果和上面是一样的。

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

系统提供了大量的 Tween:

基本上常用的属性都包含了其对应的 Tween,看一下 ColorTween 的源代码实现:

本质上也是使用 Color.lerp 实现的。

Curve

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

蓝色盒子大小 100 变大到 200,动画曲线设置为 bounceIn(弹簧效果)

class CurveDemo extends StatefulWidget {
@override
_CurveDemoState createState() => _CurveDemoState();
} class _CurveDemoState extends State<CurveDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation _animation; @override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
..addListener(() {
setState(() {});
}); _animation = Tween(begin: 100.0, end: 200.0)
.chain(CurveTween(curve: Curves.bounceIn))
.animate(_controller);
} @override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
_controller.forward();
},
child: Container(
height: _animation.value,
width: _animation.value,
color: Colors.blue,
alignment: Alignment.center,
child: Text(
'点我变大',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
} @override
void dispose() {
super.dispose();
_controller.dispose();
}
}

动画加上Curve 后,AnimationController 的最小/大值必须是 [0,1]之间,例如下面的写法就是错误的:

_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000),lowerBound: 100.0,upperBound: 200.0)
..addListener(() {
setState(() {});
});
_animation = CurveTween(curve: Curves.bounceIn).animate(_controller);

抛出如下异常:

正确写法:

_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
..addListener(() {
setState(() {});
}); _animation = Tween(begin: 100.0, end: 200.0)
.chain(CurveTween(curve: Curves.bounceIn))
.animate(_controller);

系统已经提供了38种常用到动画曲线:

linear

decelerate

bounceIn

bounceOut

elasticIn

其余动画效果可以官方文档查看。

通常情况下,这些曲线能够满足 99.99% 的需求,很多时候设计也就是告诉你动画 先快后慢 或者 先慢后快,所以选个类似的就可以了,但有一些 特别 的设计非要一个系统没有的动画曲线,要怎么办?

那就自定义一个动画曲线

其实自定义一个动画曲线难点在 数学 上,怎么把数学公式用代码实现才是难点。

下面是一个 楼梯效果 的动画曲线:

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

class _StairsCurve extends Curve {

  @override
double transformInternal(double t) {
return t;
}
}

直接返回 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;
} @override
double transformInternal(double t) {
return _perStairY * (t / _perStairX).floor();
}
}

修改开始处的案例,使用此曲线:

_animation = Tween(begin: 100.0, end: 200.0)
.chain(CurveTween(curve: _StairsCurve(5)))
.animate(_controller);

总结

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

将 AnimationController 、 Tween 、Curve 进行关联的方式:

AnimationController _controller;
Animation _animation; @override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
..addListener(() {
setState(() {});
}); _animation = Tween(begin: 100.0, end: 200.0)
.animate(_controller);
}

或者:

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

加入 Curve :

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

或者:

_animation = _controller
.drive(CurveTween(curve: Curves.linear))
.drive(Tween(begin: 100.0, end: 200.0));

只需要 Curve :

_animation = CurveTween(curve: Curves.linear)
.animate(_controller);

或者

_animation = _controller.drive(CurveTween(curve: Curves.linear));

一个 AnimationController 可以对应多个 Animation(Tween 或者 Curve),StatefulWidget 组件可以包含多个 AnimationController ,但 SingleTickerProviderStateMixin 需要修改为 TickerProviderStateMixin,改变颜色和大小,由两个 AnimationController 控制:

class MultiControllerDemo extends StatefulWidget {
@override
_MultiControllerDemoState createState() => _MultiControllerDemoState();
} class _MultiControllerDemoState extends State<MultiControllerDemo>
with TickerProviderStateMixin {
AnimationController _sizeController;
AnimationController _colorController;
Animation<double> _sizeAnimation;
Animation<Color> _colorAnimation; @override
void initState() {
super.initState();
_sizeController =
AnimationController(vsync: this, duration: Duration(milliseconds: 2000))
..addListener(() {
setState(() {});
}); _sizeAnimation = _sizeController
.drive(CurveTween(curve: Curves.linear))
.drive(Tween(begin: 100.0, end: 200.0)); _colorController =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
..addListener(() {
setState(() {});
}); _colorAnimation = _colorController
.drive(CurveTween(curve: Curves.bounceIn))
.drive(ColorTween(begin: Colors.blue, end: Colors.red));
} @override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
_sizeController.forward();
_colorController.forward();
},
child: Container(
height: _sizeAnimation.value,
width: _sizeAnimation.value,
color: _colorAnimation.value,
alignment: Alignment.center,
child: Text(
'点我变化',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
} @override
void dispose() {
super.dispose();
_sizeController.dispose();
_colorController.dispose();
}
}

AnimationController 、Tween 、Curve 是整个动画的基础,Flutter 系统封装了大量了动画组件,但这些组件也是基于此封装的,因为深入了解这三部分比学习使用动画组件更重要,再次对这3个进行总结:

  • AnimationController:动画控制器,控制动画的播放、停止等。继承自Animation< double >,是一个特殊的Animation对象,默认情况下它会线性的生成一个0.0到1.0的值,类型只能是 double 类型,不设置动画曲线的情况下,可以设置输出的最小值和最大值。
  • Curve:动画曲线,作用和Android中的Interpolator(差值器)类似,负责控制动画变化的速率,通俗地讲就是使动画的效果能够以匀速、加速、减速、抛物线等各种速率变化。
  • Tween:将 AnimationController 生成的 [0,1]值映射成其他属性的值,比如颜色、样式等。

完成一个动画效果的过程如下:

  1. 创建 AnimationController
  2. 如果需要 Tween 或者 Curve,将 AnimationController 与其关联,Tween 和 Curve 并不是必须的,当然大部分情况都需要。
  3. 将动画值作用于组件,当没有Tween 和 Curve 时,动画值来源于AnimationController,如果有 Tween 和 Curve,动画值来源于 Tween 或者Curve 的 Animation。

如果你发现阅读完此篇文章还是感觉不会写动画,不要灰心,这是正常的,第一次想了解这些抽象的概念还是比较困难的,如果你有其他平台的相关经验,那会好很多,对于动画,想要掌握个人认为只有一个方法就是 多写

后面会介绍动画组件基础使用、实现原理、高级动画以及自定义动画,把每一个动画组件的用法都亲自手写一遍(而不是复制黏贴),回过头来在看这篇文章,会有不一样的感觉。

交流

老孟Flutter博客地址(330个控件用法):http://laomengit.com

欢迎加入Flutter交流群(微信:laomengit)、关注公众号【老孟Flutter】:

【Flutter 实战】动画核心的更多相关文章

  1. 【Flutter 实战】一文学会20多个动画组件

    老孟导读:此篇文章是 Flutter 动画系列文章第三篇,后续还有动画序列.过度动画.转场动画.自定义动画等. Flutter 系统提供了20多个动画组件,只要你把前面[动画核心](文末有链接)的文章 ...

  2. 【Flutter 实战】17篇动画系列文章带你走进自定义动画

    老孟导读:Flutter 动画系列文章分为三部分:基础原理和核心概念.系统动画组件.8篇自定义动画案例,共17篇. 动画核心概念 在开发App的过程中,自定义动画必不可少,Flutter 中想要自定义 ...

  3. 《Flutter实战》开源电子书

    <Flutter实战>开源电子书 <Flutter实战> 开源了,本书为 Flutter中文网开源电子书项目,本书系统介绍了Flutter技术的各个方面,本书属于原创书籍(并非 ...

  4. 【Flutter实战】移动技术发展史

    老孟导读:大家好,这是[Flutter实战]系列文章的第一篇,这并不是一篇Flutter技术文章,而是介绍智能手机操作系统.跨平台技术的演进以及我对各种跨平台技术看法的文章. 智能手机操作系统 塞班( ...

  5. 【Flutter实战】定位装饰权重组件及柱状图案例

    老孟导读:Flutter中有这么一类组件,用于定位.装饰.控制子组件,比如 Container (定位.装饰).Expanded (扩展).SizedBox (固定尺寸).AspectRatio (宽 ...

  6. 【Flutter 实战】简约而不简单的计算器

    老孟导读:这是 [Flutter 实战]组件系列文章的最后一篇,其他组件地址:http://laomengit.com/guide/widgets/Text.html,接下来将会讲解动画系列,关注老孟 ...

  7. Flutter实战视频-移动电商-02.Flutter实战建立项目和编写入口文件

    02.Flutter实战建立项目和编写入口文件 创建项目: flutter create flutter_shop 创建完成之后呢,它会提示我们, 进入flutter_shop的目录,然后执行flut ...

  8. 《Flutter 实战》开源电子书

    <Flutter 实战>开源电子书 转 https://blog.csdn.net/OQjya206rsQ71/article/details/86619630   关于 Flutter ...

  9. Flutter实战】文本组件及五大案例

    老孟导读:大家好,这是[Flutter实战]系列文章的第二篇,这一篇讲解文本组件,文本组件包括文本展示组件(Text和RichText)和文本输入组件(TextField),基础用法和五个案例助你快速 ...

  10. 【Flutter实战】图片组件及四大案例

    老孟导读:大家好,这是[Flutter实战]系列文章的第三篇,这一篇讲解图片组件,Image有很多高级用法,希望对您有所帮助. 图片组件是Flutter基础组件之一,和文本组件一样必不可少.图片组件包 ...

随机推荐

  1. lambda表达式操作DataTable

    using System;using System.Collections.Generic;using System.Data;using System.Linq;using System.Text; ...

  2. Centos网络配置文件详解

    配置文件位置: 根目录下面的etc下面的sysconfig下面的network-scripts下面的网卡名称配置文件. /etc/sysconfig/network-scripts/网卡名称 如图:我 ...

  3. CORS跨域漏洞学习

    简介 网站如果存CORS跨域漏洞就会有用户敏感数据被窃取的风险. 跨域资源共享(CORS)是一种浏览器机制,可实现对位于给定域外部的资源的受控访问.它扩展了同源策略(SOP)并增加了灵活性.但是,如果 ...

  4. jQuery实现瀑布流布局

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  5. 解析D-Bus服务器的地址

    D-Bus 1.13.14 Main Page Related Pages Modules Data Structures Files Typedefs | Functions Address par ...

  6. make & make install(make altinstall) 因动态库gcc版本问题

    cc1: error: unrecognized command line option “-flot” 解决方式是:找到 configure之后的Makefile, 删除 -flot 原因是gcc ...

  7. SQL Msg 18054, Level 16, State 1

    今天接到一个看起来很简单的任务--修改数据库中的一项数据.听起来很简单吧. 在网上搜索了一下,很快就拼凑出了相应的 SQL 语句: UPDATE [suivi].[dbo].[numSerie]SET ...

  8. Android学习笔记颜色资源文件

    资源文件目录 颜色资源文件格式 colors.xml <?xml version="1.0" encoding="utf-8"?> <reso ...

  9. cb24a_c++_STL算法简介

    算法概述 算法部分主要由头文件<algorithm>,<numeric>和<functional>组成.       <algorithm>是所有STL ...

  10. 效率思维模式与Zombie Scrum

    Scrum是由Ken Schwaber和Jeff Sutherland在20世纪90年代提出的概念,并在1995年首次正式确定.起初Scrum是为了解决产品和软件开发固有的复杂性,然而现在Scrum被 ...