之前三篇文章的动画, 我刻意的将各种动画做成了独立式的, 即使是略微复杂的动画, 也分解成了独立式。 实际使用时, 可能一个动画会有一系列小的动画片段组成, 每个动画之间可能有时间顺序关系。
- 可以监听
AnimationController
的状态以便启动下一段动画。 - 可以通过设置每个小动画片段的时间间隔, 来控制当前哪一个动画运作起来。
其中设置小动画片段的时间间隔, 关键在于Interval
部件的使用,以及为每个动画设置一个取值范围Tween
,使用同一个AnimationController
控制总体的动画状态。
先看一个示例动画。
动画分解
从示例中可以看到这个动画大概可以分解成2个大动作:
- 头像组件从上部掉下来
- 头像原地旋转缩放,该动作又被拆分成了三个小动作, 并且循环
- 头像旋转一圈
- 头像缩小一点
- 头像放大一点
代码
import 'package:flutter/material.dart';
class MyTimeLineAnimationDemo extends StatelessWidget {
buildBackButton(BuildContext context) {
return Positioned(
left: 0.0,
top: 0.0,
right: 0.0,
child: Container(
padding: EdgeInsets.only(top: 32),
alignment: Alignment.topLeft,
child: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
MainPage(),
buildBackButton(context),
IconLayer(),
],
),
);
}
}
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.grey, Colors.black],
),
),
);
}
}
class IconLayer extends StatefulWidget {
@override
_IconLayerState createState() => _IconLayerState();
}
class _IconLayerState extends State<IconLayer> with TickerProviderStateMixin {
AnimationController posController;
Animation<double> posAnimation;
Duration posDuration;
AnimationController nController;
Duration nDuration;
Animation<double> rotationAnimation;
Animation<double> scaleDownAnimation;
Animation<double> scaleUpAnimation;
@override
void initState() {
super.initState();
// 掉落的动画控制
posDuration = Duration(milliseconds: 300);
posController = AnimationController(vsync: this, duration: posDuration)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
// 稍微延迟一下再启动后面的动画, 免得太突兀了
Future.delayed(Duration(milliseconds: 500), () {
nController.repeat();
});
}
});
posAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: posController, curve: Curves.linearToEaseOut));
//旋转和缩放的动画控制
nDuration = Duration(milliseconds: 3000);
nController = AnimationController(vsync: this, duration: nDuration);
rotationAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: nController, curve: Interval(0.0, 0.7)));
scaleDownAnimation = Tween<double>(begin: 1.0, end: 0.8).animate(
CurvedAnimation(parent: nController, curve: Interval(0.7, 0.85)));
scaleUpAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(parent: nController, curve: Interval(0.85, 1.0)));
//启动动画
posController.forward();
}
@override
void dispose() {
posController?.dispose();
nController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
double height = MediaQuery.of(context).size.height - 120;
return AnimatedPositioned(
right: 10,
top: height * posAnimation.value,
duration: posDuration,
child: Container(
width: 80,
height: 80,
margin: EdgeInsets.all(8.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5),
),
child: RotationTransition(
turns: rotationAnimation,
child: ScaleTransition(
scale: scaleDownAnimation,
child: ScaleTransition(
scale: scaleUpAnimation,
child: CircleAvatar(
backgroundImage: AssetImage('assets/images/head.jpg'),
),
),
),
),
),
);
}
}
分析
主界面用Stack
串联了三部分组成, 分别是
- MainPage, 就是一个简单的渐变背景,
- 回退(back button)按钮,
- 动画图标
第一段掉落的动画, 采用的是AnimatedPositioned
组件来实施, AnimatedPositioned
组件只能在Stack
组件中使用。
第二段原地旋转缩放的动画, 可以看到控制器只有一个:nController
, 但Animation
有三个, 分别是:
- rotationAnimation
- scaleDownAnimation
- scaleUpAnimation
通过Interval部件将三个Animation
分解成了三个时间段, 分别是
- Interval(0.0, 0.7)
- Interval(0.7, 0.85)
- Interval(0.85, 1.0)
意思是在动画开始到0.7的时间段内旋转动画在起作用, 0.7-0.85的时间段内, 缩小, 0.85-结束的时间段内, 放大。
第一段和第二段动画之间的衔接, 靠的是监听第一段动画的状态,当监听到第一段动画完成后, 延迟一小段时间, 然后第二段动画重复(repeat)。
if (status == AnimationStatus.completed) {
// 稍微延迟一下再启动后面的动画, 免得太突兀了
Future.delayed(Duration(milliseconds: 500), () {
nController.repeat();
});
}