在使用Animation之前,先了解一下,要完成一个完整的动画,需要用到哪些结构(类)
-
Animation :Animation是一个抽象类,定义
abstract class Animation<T> extends Listenable implements ValueListenable<T>
,继承Listenable
类(可以添加监听和移除监听);实现ValueListenable
接口(存在一个get value 的方法) -
Curve:曲线,描述动画的过程,比如匀速执行,加速执行,正弦、余弦函数等执行;在
class Curves
中可以找到一些常用的曲线:
class Curves {
static const Curve linear = _Linear._();
static const Curve decelerate = _DecelerateCurve._();
static const Cubic fastLinearToSlowEaseIn = Cubic(0.18, 1.0, 0.04, 1.0);
static const Cubic ease = Cubic(0.25, 0.1, 0.25, 1.0);
...
}
- AnimationController:继承
Animation<double>
用来控制动画启动forward()、停止stop() 、反向播放 reverse()等,AnimationController 会在每一帧生成一个值(在指定的时间段内线性的生成从0.0到1.0的值),创建一个AnimationController:
AnimationController controller = AnimationController(duration: Duration(seconds: 3), vsync: this);
在三秒钟内,每一帧生成 0.0 到 0.1 的值
一:绑定曲线与动画
在上面,我们讲了一个动画的主要组成结构,接下来,我们要将这些结构组合形成动画。
1.1:CurvedAnimation 绑定曲线与动画
CurvedAnimation
与AnimationController
一样也继承 Animation<double>
,下面开始使用CurvedAnimation
来连接曲线与动画
- 创建 AnimationController
AnimationController _controller;
_controller = AnimationController(duration: Duration(seconds: 60), vsync: this);
// 这里了解一下 AnimationController 的参数
duration: 动画执行的时长,就是动画需要在60秒的时间内完成
vsync:TickerProvider 类型,监听屏幕刷新的回调,因为动画本质就是每帧绘制不同的界面来实现的。所以我们理解,动画需要和屏幕的刷新帧数关联,一般我们通过 with SingleTickerProviderStateMixin 来实现
- 创建 CurvedAnimation
CurvedAnimation _animation;
_animation = CurvedAnimation(curve: Curves.ease, parent: _controller);
// CurvedAnimation 的参数了解
curve:曲线,需要传入一个曲线对象,用来表示动画是匀速、加速等执行效果
parent:这里传入动画控制 AnimationController,就是绑定曲线与动画
二:呈现动画(一般有三种方式)
上面我们通过 CurvedAnimation 已经将 动画与曲线绑定在一起了:在指定的时间内,每帧都会按照指定的曲线生成一个值value,这个value就是我们控制动画效果的值
2.1:第一种方式实现动画
我们可以通过监听动画的value来实时去刷新界面,这样就可以实现动画的效果了,监听动画的value,然后不断去刷新界面,前面讲到 Animation
继承 Listenable
,所以我们可以添加如下监听:
_animation.addListener(() {
setState(() { });
});
完整的实列如下:点击按钮,动画开始执行,Icon会不断放大,并显示出动画的value值
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
AnimationController _controller;
CurvedAnimation _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: Duration(seconds: 20), vsync: this);
_animation = CurvedAnimation(curve: Curves.ease, parent: _controller);
_animation.addListener(() {
setState(() {
});
});
}
void _incrementCounter() {
if (_controller.isAnimating) {
_controller.stop();
}else {
_controller.forward();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'${_animation.value}',
),
Icon(Icons.home, size: 24 + _animation.value * 100)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.star),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
2.2:第二种方式 AnimatedWidget
在 2.1 中我们利用Animation的监听,然后不断调用 setState 方法实现了动画效果,那么有没有更优雅的方式?
使用AnimatedWidget
简化一下,需要继承AnimatedWidget
:
第一步:将存在动画效果的 widge 进行拆分,上面的效果在 Icon 上,我们就将 Icon 拆出来:
class IconAnimatedWidget extends AnimatedWidget {
IconAnimatedWidget({
Key key,
Animation<double> animation
}) : super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return Icon(
Icons.home,
size: 24 + animation.value * 100
);
}
}
下面开始使用IconAnimatedWidget
,修改 build 方法如下:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconAnimatedWidget(
animation: _animation,
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.star),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
可以看出,使用 AnimatedWidget
之后,我们不需要去手动监听和调用setState
;主要是 AnimatedWidget
在内部帮我做了这些操作,我们可以找到 AnimatedWidget
对应的状态管理 _AnimatedState
部分代码如下:
class _AnimatedState extends State<AnimatedWidget> {
@override
void initState() {
super.initState();
widget.listenable.addListener(_handleChange);
}
...
void _handleChange() {
setState(() {
// The listenable's state is our build state, and it changed already.
});
}
...
}
2.3:第三种方式 AnimatedBuilder
如果我们不想将 Animation 的 widget 抽离出去,但是又不想去显示的调用 setState,那么我们还可以借助 AnimatedBuilder 来实现动画效果。
注意:AnimatedBuilder
也是继承AnimatedWidget
AnimatedBuilder
的构建方法如下:
class AnimatedBuilder extends AnimatedWidget {
/// Creates an animated builder.
///
/// The [animation] and [builder] arguments must not be null.
const AnimatedBuilder({
Key? key,
required Listenable animation,
required this.builder,
this.child,
}) : assert(animation != null),
assert(builder != null),
super(key: key, listenable: animation);
// 参数了解
animation:与2.2 中一样,我们需要传入一个动画
child:这里传入child,其实是为了防止child被多次build
builder:回调函数,需要我们返回一个 widget(这个 widget 就是我们需要执行动画的widget )
...
}
现在我们利用 AnimatedBuilder
改写上面的代码:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Icon(
Icons.home,
size: 24 + _animation.value * 100,
);
},
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.star),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
这里我们使用 AnimatedBuilder
,但是未指定他的child
,是因为我们的动画效果就在builder
中返回的 widget上,如果不是在返回的 widget 上,我们通常如下使用:
AnimatedBuilder(
animation: _animation,
child: Icon(Icons.home),
builder: (context, child) {
return Container(
width: 24 + _animation.value * 100,
height: 24 + _animation.value * 100,
child: child,
color: Colors.green,
);
},
)
这样使用的话,意味着Container
使用了动画,不断变大,但是Icon(Icons.home)
的大小不会改变。
这样的好处:就是动画每帧重构(build)被限定在 builder 回调函数中,并且指定的 child 被保存起来,不会被重构(build) 。这里只对Container
这个widget进行了重构(build),而内部的child 只是赋值,并不会执行重构(build)函数。
现在我们比对一下以上三种方式:
- 第一种方式比较直观:手动设置监听调用setState方法,通常很少用,因为看起来比较low,而且每帧都需要 build 整个界面
- 第二种方式适合用来拆分widget、比如复杂的界面,通常都需要我们去拆分出一些单独的widget
- 第三种方式适合简单的界面,如果觉得不需要去拆分成多个widget,此方式比较适合;另外如果需要执行动画的 widget 存在子widget,且子widget不需要跟随动画改变,此方式也非常适合
- 第二种和第三种有明显的优势,因为
AnimatedBuilder
继承StatefulWidget
,则每帧需要重新 build 只会执行AnimatedBuilder
的 build 方法,而不需要 build 当前整个界面;
三:Animation
在上面的操作中,我们提到 AnimationController 继承 Animation
,所有我们在接收animation.value 时都是 double 类型,那么如果我想动画改变颜色、padding或者其他该怎么办?
3.1:Tween 映射 Animation 的value 值
先了解Tween
的定义,Tween
继承 Animatable
,在 Animatable
抽象类中,我们可以看到官方的解释: 给定[AnimationT transform(double t);
;接着我们去看 Tween
,在 Tween
对抽象方法进行了实现
@override
T transform(double t) {
if (t == 0.0)
return begin as T;
if (t == 1.0)
return end as T;
return lerp(t);
}
但是通常我们使用 Tween
的子类,比如 ColorTween
、SizeTween
...
我们可以通过Tween
类将animation.value
的值映射成其他类型,比如Colors,我们创建如下 Tween
final Tween colorTween = ColorTween(begin: Colors.red, end: Colors.black);
比如RectTween,
final Tween paddingTween = RectTween(begin: Rect.zero, end: Rect.fromLTRB(10, 10, 10, 10));
3.2:将 Tween 绑定到动画中
调用
Animatable
提供的animate()
方法,我们可以将Tween
绑定到动画中,动画20秒内由红色变成黑色
- 不指定动画曲线绑定:
final AnimationController controller = AnimationController(duration: Duration(seconds: 20), vsync: this);
final Tween colorTween = ColorTween(begin: Colors.red, end: Colors.black);
final Animation<Color> colorAnimation = colorTween.animate(controller);
- 指定动画曲线绑定:
final AnimationController controller = AnimationController(duration: Duration(seconds: 20), vsync: this);
CurvedAnimation curvedAnimation = CurvedAnimation(curve: Curves.easeIn, parent: controller);
final Tween colorTween = ColorTween(begin: Colors.red, end: Colors.black);
final Animation<Color> colorAnimation = colorTween.animate(curvedAnimation);
3.3:使用 colorAnimation 的 value
替换上面的 animation.value,改为 colorAnimation.value 如下:
AnimatedBuilder(
animation: _curvedAnimation,
child: Icon(Icons.home),
builder: (context, child) {
return Container(
width: 100,
height: 100,
child: child,
color: _colorAnimation.value,
);
},
)
效果截图:
###四:动画状态
动画执行执行后,我们可能需要监听动画的执行状态,动画是否完成、动画完成后停止在开头还是结尾等...
在前面,我们知道 Animation
类有监听动画帧回调void addListener(VoidCallback listener);
的方法,其实还有有个方法 void addStatusListener(AnimationStatusListener listener);
,我们可以通过设置此监听方法得到动画的执行状态:
_colorAnimation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
print('动画执行完毕,停止在动画结束');
// 动画执行完成,反向执行此动画
_controller.reverse();
}else if (status == AnimationStatus.dismissed) {
print('动画执行完毕,停止在动画开始');
// 动画执行完成,正向执行此动画
_controller.forward();
}else if (status == AnimationStatus.forward) {
print('动画从开始到结束,正向执行');
}else if (status == AnimationStatus.reverse) {
print('动画从结束到开始,反向执行');
}
});
关于 AnimationStatus
的枚举类型:
/// The status of an animation.
enum AnimationStatus {
/// The animation is stopped at the beginning.
dismissed, // 动画在起始点停止
/// The animation is running from beginning to end.
forward, // 动画正在正向执行
/// The animation is running backwards, from end to beginning.
reverse, // 动画正在反向执行
/// The animation is stopped at the end.
completed, // 动画在终点停止
}
控制动画的一些常用方法:
controller.forward(); // 正向执行此动画
controller.reverse(); // 反向执行此动画
controller.stop(); // 停止执行此动画
controller.dispose(); // 销毁此动画(销毁后无法开启,一般在界面被销毁时调用)
controller.isAnimating // 判断此动画是否正在执行
五:路由切换动画
对于页面切换,flutter提供了一个路由的管理类 Navigator,我们通过Navigator.of(context).push() 来实现页面的切换。
5.1:常用的切换方式
MaterialPageRoute
根据系统不同,展示系统的切换效果
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return RouterPage1();
}));
5.2:指定切换效果为iOS效果
CupertinoPageRoute
指定iOS的效果
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return RouterPage1();
}));
从底部弹出效果,指定fullscreenDialog
为true
,采用Dialog
的效果(从底部弹出)
Navigator.of(context).push(CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) {
return RouterPage1();
}
));
5.3:修改为其他效果
前面提到的MaterialPageRoute
和CupertinoPageRoute
都系统帮我们定义好的Route(已经在内部实现了过渡动画),如果我们想要实现其他的效果,如何实现?
flutter 本身提供了很多的过渡效果,比如 FadeTransition 透明度渐变效果,我们需要使用PageRouteBuilder
来获取 Route 切换的帧动画值 animation;并返回一个 FadeTransition
动画效果的widget
Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return FadeTransition(
opacity: animation,
child: RouterPage1(),
);
}
));