作者能力有限, 如果您在阅读过程中发现任何错误, 还请您务必联系本人,指出错误, 避免后来读者再学习错误的知识.谢谢!
参考: https://flutter.dev/docs/development/ui/animations/tutorial
基本概念
Animation: Flutter 提供的核心动画库, 使用指定的值来构建动画.
Animation 实例: 它只知道动画当前的状态(state)(比如,动态是否启动,结束,或者正在执行), 但是不知道当前屏幕显示的内容.
CurveAnimation: 用于定义一个非线性曲线.
Tween: 补间动画,用于在动画状态之间插入补充的值。 比如,它可以用与在从红色变成绿色的动画中插入中间值.
Listensers 和 StatusListeners: 用于监听动画状态(state)的改变.
下面简要介绍一下常用的 Animation.
Animation<double>
Animation 的实例会在给定的两个值之间生成连续的中间值。 生成数据的间隔由 duration 决定. 生成连续值的方式可以有以下几种: 线性,曲线,阶跃,或者其他方法.
Animation 实例是有状态的, 可以通过 .value 字段获取它当前的状态值.
Animtation 实例无法知道 build 函数的状态和其他绘制状态.
CurvedAnimation
CurvedAnimation 用于定义一个非线性动画.
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
Curves 类中定义了许多常用的曲线
CurvedAnimation 和 AnimationController 类型都是 Animation 的子类型. 因此你可以将他们赋值给 Animation.
AnimationController
AnimationController 是一个特殊的 Animation, 用户生成新的值在硬件准备好之后. 默认情况下, 它会已特定间隔生成一系列间与 0.0 - 1.0 的值.
例如:
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
正如前文所说,AnimationController 是 Animation 的子类,相比于基类,它提供了一些额外的方法来控制动画:
比如,你可以使用 forward()
来开始一段动画.
生成数据的频率由屏幕刷新次数决定. 通常情况下是每分钟 60 次.
每当新数据生成时,每个 Animation 实例的 listenser 会被调用.
当创建一个动画对象时,需要传入一个 vsync
参数. 使用 vsync
可以防止屏幕外的动画消耗不必要的资源。
Tween
通常情况下, AnimationController 实例的值范围为 0.0 - 1.0. 当你需要使用其他范围的值时, 你可以通过使用 Tween 来配置动态来生成不同范围的值.
比如: 当你想指定值的范围为 -200 - 0.0
tween = Tween<double>(begin: -200, end: 0);
Tween 是一个无状态的对象(stateless), 它仅仅知道 begin 和 end. 它的任务通常是用来将一个值范围映射为另外一个值范围. 被映射的值范围通常都是 0.0 - 1.0.
Tween 是 Animatable 的子类,注意不是 Animation. Animatable 不需要生成的 double 值的输出.
比如: ColroTween 用于生成在两个 Color 之间的动画.
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween 实例不保存任何状态,但是它提供了 evalute(Animation animation)
方法. 这个方法可以用于映射当前动画值. 当前动画值可以通过 Animation 实例的 value 字符访问.
Tween.animate
可以通过将 controller 对象传给 Tween.animate
方法来使用一个 Tween.
比如: 如下代码在 500 ms 中生成 0 - 255 之间的值.
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
// 注意,animate 返回一个 Animation 对象,而不是 Animatable 对象.
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
Animation Notification
Animation 实例可以由它的 Listenser 和 StatusListener,分别通过 addListener()
和 addStatusListener()
方法添加.
Listenser 将会被回调当动画值改变的时候. 最常见的做法是在 Listener 中调用 setState()
来重绘页面.
StatusListener 将会被调用当动画开始, 结束,前进,后退的时候.
动画实例
Rendering animations
一个显示动画 Flutter Logo 的实例
原始代码,无动画
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 300,
width: 300,
child: FlutterLogo(),
),
);
}
}
接下来,修改代码,让这个 logo 从没有到占满屏幕
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
setState(() {});
});
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
我们使用 addListener 调用 setState
每次 Animation 生成一个新的值之后会调用我们添加的 listener, 通过调用 setState 将当前帧标记为 dirty, 之后 Flutter 就会重绘整个帧.
在 build 方法中, 将 container 的 height 和 width 指定为 animation 当前的值,这样每次动画值更新后,container 的大小就会跟着变化.
在 initState 中初始话 controller, 在 dispose 中释放 controller, 以避免内存泄漏.
效果如下:
Simplifying with AnimatedWidget
这里我们使用 AnimatedWidget 来简化上一步的代码, 将动画代码从 widget 的构建代码中分离出来。
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
}
Monitoring the progress of the animation
本实例将使用 addStatusListener 来监听动画的状态, 实现一个呼吸动画.
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
})
..addStatusListener((status) => print('$status'));
controller.forward();
}
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
}
主要添加了 18-36 行的代码.
效果如下:
Refactoring with AnimatedBuilder
上面代码中存在一个问题, 那就是动画改变时会同时修改包含它的页面.
一个更好的解决方案是将绘制动画的逻辑由另外一个类型负责.
我们使用 AnimatedBuilder 类来完成该功能. 就像 AnimatedWidget 一样, AnimatedBuilder 会自动监听动画的状态, 标记当前 widget
为 dirty 在适当的时候, 然后重绘动画. 我们不再需要调用 addListener.
为此,我们就构建如下的 widget tree:
LogoWidget 的实现非常简单, 仅仅显示一个 Flutter Logo.
我们在 GrowTransition 的 build 方法中构建中间的 widgets.
完整代码如下:
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) {
return GrowTransition(
child: LogoWidget(),
animation: animation,
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class LogoWidget extends StatelessWidget {
// Leave out the height and width so it fills the animating parent
Widget build(BuildContext context) => Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: FlutterLogo(),
);
}
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) => Center(
/*
* One tricky point in the code below is that the child looks like it’s specified twice.
* What’s happening is that the outer reference of child is passed to AnimatedBuilder,
* which passes it to the anonymous closure, which then uses that object as its child.
* The net result is that the AnimatedBuilder is inserted in between the two widgets in the render tree.
*/
child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
height: animation.value,
width: animation.value,
child: child,
),
child: child),
);
}
Simultaneous animations
这一节,我们在 “monitoring the progress of the animation” 小节的基础上构建一个同时变化的动画.
我们的 Logo 动画将在改变大小的同时改变自身的透明度.
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
controller.forward();
}
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class AnimatedLogo extends AnimatedWidget {
// Make the Tweens static because they don't change.
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity (
opacity: _opacityTween.evaluate(animation), // 通过evaluate 获取 Tween 当前的值
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: FlutterLogo(),
),
),
);
}
}
效果如下:
END!