1. 基本动画
一个 Widget 的属性
在一定时间
内发生改变
就形成了动画,所以构建一个动画需要三个要素:
- 属性起始值:需要哪个属性,从哪个值变化到哪个值
- 时长:需要在多长时间内完成变化
- 控制器:让动画开始
controller.forward
,或者让动画反向执行controller.reverse
,或者执行到指定时间点controller.animateTo
下面写一个让图片左右移动的动画
- 使用动画需要 with SingleTickerProviderStateMixin
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{
...
}
- 创建 AnimationController,即
控制器
,包含了时长
AnimationController controller = AnimationController(duration: Duration(milliseconds: 700), vsync: this);
- 指定属性起始值
Offset _offset = Offset(0.0, 0.0);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Transform.translate(offset: _offset, child: FlutterLogo(size: 100))
),
);
}
}
- 监听 controller.value 修改属性值
controller.addListener(() {
setState(() {
_offset = Offset(controller.value * 150, 0.0);
});
- 执行动画
...
floatingActionButton: FloatingActionButton(
onPressed: () => controller.forward(),
),
- 还可以指定插值器
AnimationController 的值是从0到1线性变化的,可以将 controller 和一个 Curve 插值器 “连” 起来,让其变成 “非线性”
Animation animation = controller.drive(CurveTween(curve: Curves.bounceIn));
animation.addListener(() {
setState(() {
_offset = Offset(animation.value * 150, 0.0);
});
});
效果如图:
完整代码:
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
AnimationController controller;
Offset _offset = Offset(0.0, 0.0);
Animation animation;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: Duration(milliseconds: 2000), vsync: this);
// controller.addListener(() {
// setState(() {
// _offset = Offset(controller.value * 150, 0.0);
// });
// });
animation = controller.drive(CurveTween(curve: Curves.bounceIn));
animation.addListener(() {
setState(() {
_offset = Offset(animation.value * 150, 0.0);
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Transform.translate(
offset: _offset,
child: FlutterLogo(
size: 100,
)),
),
floatingActionButton: FloatingActionButton(
onPressed: () => controller.forward(from: 0.0),
),
);
}
}
2. ImplicitlyAnimatedWidget
Flutter 提供了一种极其方便的实现动画的方式,如 官方 Widget Catelog 提到的比如 AnimatedContainer,AnimatedCrossFade 等,他们都继承自ImplicitlyAnimatedWidget,所以原理都是一样的,通过一个“开关” 自动执行“状态一”到“状态二”的动画。
eg. AnimatedCrossFade
Widget getCurrentList(int index) {
return AnimatedCrossFade(
crossFadeState:
index == 0 ? CrossFadeState.showFirst : CrossFadeState.showSecond,//开关 ? 状态一 : 状态二
duration: Duration(milliseconds: 500),
firstChild: ListView.builder(...),
secondChild: ListView.builder(...),
);
在调用getCurrentList(int i)
时传递不同的值,widget会自动执行渐现动画。
同理在 AnimatedContainer 中也可以通过类似的三个主要参数构建动画(代码来自官方文档):
AnimatedContainer(
width: selected ? 200.0 : 100.0,//开关 ? 状态一 : 状态二
height: selected ? 100.0 : 200.0,
color: selected ? Colors.red : Colors.blue,
alignment: selected ? Alignment.center : AlignmentDirectional.topCenter,
duration: Duration(seconds: 2),
curve: Curves.fastOutSlowIn,
child: FlutterLogo(size: 75),
),
3. TransitionWidget
Flutter 还有一些名称中带 “Transition” 的 widget,如最常用的 SlideTransition
Animation<Offset> pickerAnimation = Tween<Offset>(begin: Offset(0.0, 1.0), end: Offset(0.0, 0.0)).chain(CurveTween(curve: Curves.ease)).animate(pickerController);
AnimationController pickerController = AnimationController(vsync: this, duration: Duration(milliseconds: 700));
...
SlideTransition(
position: pickerAnimation
child: CusDatePicker(
...
),
),
...
pickerController.forward(from: 0.0);
用 position
参数指定一个动画,用 controller.forward()
执行动画
同理还有 FadeTransition,RotationTransition 等,都是在构建Widget时通过一个Animation类型参数指定一个动画,当动画执行时,Widget的值会相应地改变。
4. AnimatedBuilder
AnimatedBuilder 将 构造AnimationController
,设置监听器
,修改状态值
集合在一起,简化了操作,比如如下代码,跟先 new AnimationController,再在 Controller 的 Listener 中通过 setState 修改 Container 的属性值效果一样。
AnimatedBuilder(
animation: _controller,
child: Container(width: 200.0, height: 200.0, color: Colors.green),
builder: (BuildContext context, Widget child) {
return Transform.rotate(
angle: _controller.value * 2.0 * math.pi,
child: child,
);
},
);
5. Hero动画
Hero动画就是Android中的共享元素动画,实现方式也类似,都是给两个元素指定相同的tag
即可。
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: GestureDetector(
onTap: () => Navigator.push(
context, MaterialPageRoute(builder: (_) => SecondRoute())),
child: Hero(
tag: 'fly',
child: FlutterLogo(
size: 100,
),
),
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Hero(
tag: 'fly',
child: FlutterLogo(
size: 200,
),
),
),
),
);
}
}
6. Staggered Animation
-
staggered:
交错的,错列的
故名思义,就是指多个动画交错执行。官方文档 讲的比较清楚。
简单说就是通过Interval
指定动画在整个动画时长内的哪段时间执行。
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<Offset> transAnim;
Animation scaleAnim;
@override
void initState() {
super.initState();
//控制器
controller =
AnimationController(duration: Duration(seconds: 2), vsync: this);//动画总时长 2 秒
//平移动画
transAnim = Tween<Offset>(begin: Offset(0.0, 0.0), end: Offset(100.0, 0.0))
.animate(CurvedAnimation(
parent: controller,
curve: Interval(0.0, 0.6, curve: Curves.bounceIn))); //在前十分之六的时间执行平移动画,也就是 0 秒到 1.2 秒
//放大动画
scaleAnim = Tween(begin: 1.0, end: 3.0).animate(
CurvedAnimation(parent: controller, curve: Interval(0.5, 1.0))); //后一半的时间执行放大的动画,也就是 1 秒到 2 秒
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: GestureDetector(
onTap: () => controller.forward(from: 0.0), //执行动画
child: AnimatedBuilder(
animation: transAnim,
builder: (_, child) {
return Transform.translate(
offset: transAnim.value, //给平移属性赋值
child: Transform.scale(scale: scaleAnim.value,//给缩放属性赋值,
child: child),
);
},
child: FlutterLogo(
size: 100,
),
),
),
),
);
}
}
7. 路由跳转动画
Flutter提供了两种自带跳转 MaterialRoutePage
,CupertinoRoutePage
,如下图
自定义缩放动画:
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.of(context).push(
PageRouteBuilder(pageBuilder: (_, a1, a2) {
return SecondRoute();
}, transitionsBuilder: (_, a1, a2, child) {
return ScaleTransition(
scale: a1.drive(Tween(begin: 0.0, end: 1.0)),
child: child,
);
},
transitionDuration: Duration(seconds: 1)),
)),