Animation、AnimationController 与 Listener
动画就是动起来的画面,是静态的画面根据事先定义好的规律,在一定时间内不断微调,产生变化效果。而动画实现由静止到动态,主要是靠人眼的视觉残留效应。所以,对动画系统而言,为了实现动画,它需要做三件事儿:
- 确定画面变化的规律;
- 根据这个规律,设定动画周期,启动动画;
- 定期获取当前动画的值,不断地微调、重绘画面。
这三件事情对应到 Flutter 中,就是 Animation、AnimationController 与 Listener:
- Animation 是 Flutter 动画库中的核心类,会根据预定规则,在单位时间内持续输出动画的当前状态。Animation 知道当前动画的状态(比如,动画是否开始、停止、前进或者后退,以及动画的当前值),但却不知道这些状态究竟应用在哪个组件对象上。换句话说,Animation 仅仅是用来提供动画数据,而不负责动画的渲染。
- AnimationController 用于管理 Animation,可以用来设置动画的时长、启动动画、暂停动画、反转动画等。
- Listener 是 Animation 的回调函数,用来监听动画的进度变化,我们需要在这个回调函数中,根据动画的当前值重新渲染组件,实现动画的渲染。
我们看一个具体的案例:让大屏幕中间的 Flutter Logo 由小变大。
- 首先,我们初始化了一个动画周期为 1 秒的、用于管理动画的 AnimationController 对象,并用线性变化的 Tween 创建了一个变化范围从 50 到 200 的 Animaiton 对象。
- 然后,我们给这个 Animaiton 对象设置了一个进度监听器,并在进度监听器中强制界面重绘,刷新动画状态。
- 接下来,我们调用 AnimationController 对象的 forward 方法,启动动画
class _AnimateAppState extends State<AnimateApp> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
@override
void initState() {
super.initState();
//创建动画周期为1秒的AnimationController对象
controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000));
// 创建从50到200线性变化的Animation对象
animation = Tween(begin: 50.0, end: 200.0).animate(controller)
..addListener(() {
setState(() {}); //刷新界面
});
controller.forward(); //启动动画
}
...
}
需要注意的是,我们在创建 AnimationController 的时候,设置了一个 vsync 属性。这个属性是用来防止出现不可见动画的。vsync 对象会把动画绑定到一个 Widget,当 Widget 不显示时,动画将会暂停,当 Widget 再次显示时,动画会重新恢复执行,这样就可以避免动画的组件不在当前屏幕时白白消耗资源。
- 我们在一开始提到,Animation 只是用于提供动画数据,并不负责动画渲染,所以我们还需要在 Widget 的 build 方法中,把当前动画状态的值读出来,用于设置 Flutter Logo 容器的宽和高,才能最终实现动画效果:
@override
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Center(
child: Container(
width: animation.value, // 将动画的值赋给widget的宽高
height: animation.value,
child: FlutterLogo()
)));
}
- 最后,别忘了在页面销毁时,要释放动画资源:
@override
void dispose() {
controller.dispose(); // 释放资源
super.dispose();
}
面用到的 Tween 默认是线性变化的,但可以创建 CurvedAnimation 来实现非线性曲线动画。CurvedAnimation 提供了很多常用的曲线,比如震荡曲线 elasticOut:
//创建动画周期为1秒的AnimationController对象
controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000));
//创建一条震荡曲线
final CurvedAnimation curve = CurvedAnimation(
parent: controller, curve: Curves.elasticOut);
// 创建从50到200跟随振荡曲线变化的Animation对象
animation = Tween(begin: 50.0, end: 200.0).animate(curve)
AnimatedWidget 与 AnimatedBuilder
在为 Widget 添加动画效果的过程中我们不难发现,Animation 仅提供动画的数据,因此我们还需要监听动画执行进度,并在回调中使用 setState 强制刷新界面才能看到动画效果。考虑到这些步骤都是固定的,Flutter 提供了两个类来帮我们简化这一步骤,即 AnimatedWidget 与 AnimatedBuilder。
- 在构建 Widget 时,AnimatedWidget 会将 Animation 的状态与其子 Widget 的视觉样式绑定。要使用 AnimatedWidget,我们需要一个继承自它的新类,并接收 Animation 对象作为其初始化参数。然后,在 build 方法中,读取出 Animation 对象的当前值,用作初始化 Widget 的样式。
class AnimatedLogo extends AnimatedWidget {
//AnimatedWidget需要在初始化时传入animation对象
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
//取出动画对象
final Animation<double> animation = listenable;
return Center(
child: Container(
height: animation.value,//根据动画对象的当前状态更新宽高
width: animation.value,
child: FlutterLogo(),
));
}
}
- 在使用时,我们只需把 Animation 对象传入 AnimatedLogo 即可,再也不用监听动画的执行进度刷新 UI 了:
MaterialApp(
home: Scaffold(
body: AnimatedLogo(animation: animation)//初始化AnimatedWidget时传入animation对象
));
将动画和渲染职责分离:logo 作为外部参数传入,只做显示;而尺寸的变化动画则由另一个类去管理。这个分离工作,我们可以借助 AnimatedBuilder 来完成。
与 AnimatedWidget 类似,AnimatedBuilder 也会自动监听 Animation 对象的变化,并根据需要将该控件树标记为 dirty 以自动刷新 UI。事实上,如果你翻看源码,就会发现 AnimatedBuilder 其实也是继承自 AnimatedWidget。
MaterialApp(
home: Scaffold(
body: Center(
child: AnimatedBuilder(
animation: animation,//传入动画对象
child:FlutterLogo(),
//动画构建回调
builder: (context, child) => Container(
width: animation.value,//使用动画的当前状态更新UI
height: animation.value,
child: child, //child参数即FlutterLogo()
)
)
)
));
hero 动画
如何实现在两个页面之间切换的过渡动画呢?
这样的跨页面共享的控件动画效果有一个专门的名词,即“共享元素变换”(Shared Element Transition)。
通过 Hero,我们可以在两个页面的共享元素之间,做出流畅的页面切换效果。
- 为了实现共享元素变换,我们需要将这两个组件分别用 Hero 包裹,并同时为它们设置相同的 tag “hero”。然后,为 page1 添加点击手势响应,在用户点击 logo 时,跳转到 page2:
class Page1 extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(//手势监听点击
child: Hero(
tag: 'hero',//设置共享tag
child: Container(
width: 100, height: 100,
child: FlutterLogo())),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (_)=>Page2()));//点击后打开第二个页面
},
)
);
}
}
class Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Hero(
tag: 'hero',//设置共享tag
child: Container(
width: 300, height: 300,
child: FlutterLogo()
))
);
}
}