Flutter动画体系
基础概念
Flutter中也对动画进行了抽象,主要涉及Animation、Curve、Controller、Tween这四个角色,它们一起配合来完成一个完整动画
Animation | 它主要的功能是保存动画的插值和状态,根据Animation 对象的控制方式,动画可以正向运行(从起始状态开始,到终止状态结束),也可以反向运行,甚至可以在中间切换方向 |
Curve | Flutter中通过Curve (曲线)来描述动画过程,我们把匀速动画称为线性的(Curves.linear),而非匀速动画称为非线性的 |
AnimationController | AnimationController 用于控制动画,它包含动画的启动forward() 、停止stop() 、反向播放 reverse() 等方法。AnimationController 会在动画的每一帧,就会生成一个新的值。 |
Tween | 构建UI的动画值在不同的范围或不同的数据类型,则可以使用Tween 来添加映射以生成不同的范围或数据类型的值 |
Animation
Animation有如下两个方法:
addStatusListener() | 它可以给Animation 添加“动画状态改变”监听器;动画开始、结束、正向或反向时会调用状态改变的监听器 |
addListener() | 它可以用于给Animation 添加帧监听器,在每一帧都会被调用。帧监听器中最常见的行为是改变状态后调用setState() 来触发UI重建 |
Curve
我们可以通过CurvedAnimation
来指定动画的曲线,如:
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeIn);
Curves曲线 | 动画过程 |
---|---|
linear | 匀速的 |
decelerate | 匀减速 |
ease | 开始加速,后面减速 |
easeIn | 开始慢,后面快 |
easeOut | 开始快,后面慢 |
easeInOut | 开始慢,然后加速,最后再减速 |
自定义Curve
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
AnimationController
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 20.0,
vsync: this
);
生成数字的区间可以通过lowerBound
和upperBound
来指定
额外:
Ticker:当创建一个AnimationController
时,需要传递一个vsync
参数,它接收一个TickerProvider
类型的对象
Flutter应用在启动时都会绑定一个SchedulerBinding
,通过SchedulerBinding
可以给每一次屏幕刷新添加回调,而Ticker
就是通过SchedulerBinding
来添加屏幕刷新回调,这样一来,每次屏幕刷新都会调用TickerCallback
。使用Ticker
(而不是Timer
)来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源,因为Flutter中屏幕刷新时会通知到绑定的SchedulerBinding
,而Ticker
是受SchedulerBinding
驱动的,由于锁屏后屏幕会停止刷新,所以Ticker
就不会再触发。
Tween
final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);
Tween
构造函数需要begin
和end
两个参数。Tween
的唯一职责就是定义从输入范围到输出范围的映射。输入范围通常为[0.0,1.0],但这不是必须的,我们可以自定义需要的范围。
final Tween colorTween =
new ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween
对象不存储任何状态,相反,它提供了evaluate(Animation<double> animation)
方法,它可以获取动画当前映射值。 Animation
对象的当前值可以通过value()
方法取到。evaluate
函数还执行一些其它处理,例如分别确保在动画值为0.0和1.0时返回开始和结束状态。
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);
基本实现
class ScaleAnimationRoute extends StatefulWidget {
@override
_ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}
//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin{
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//图片宽高从0变到300
animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
..addListener(() {
setState(()=>{});
});
//启动动画(正向执行)
controller.forward();
}
@override
Widget build(BuildContext context) {
return new Center(
child: Image.asset("imgs/avatar.png",
width: animation.value,
height: animation.value
),
);
}
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
使用AnimatedWidget简化
class AnimatedImage extends AnimatedWidget {
AnimatedImage({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return new Center(
child: Image.asset("imgs/avatar.png",
width: animation.value,
height: animation.value
),
);
}
}
class ScaleAnimationRoute1 extends StatefulWidget {
@override
_ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//图片宽高从0变到300
animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
//启动动画
controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedImage(animation: animation,);
}
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
动画状态监听
枚举值 | 含义 |
---|---|
dismissed | 动画在起始点停止 |
forward | 动画正在正向执行 |
reverse | 动画正在反向执行 |
completed | 动画在终点停止 |
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 1), vsync: this);
//图片宽高从0变到300
animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
//动画执行结束时反向执行动画
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
//动画恢复到初始状态时执行动画(正向)
controller.forward();
}
});
//启动动画(正向)
controller.forward();
}
过渡动画
Navigator.push(context, CupertinoPageRoute(
builder: (context)=>PageB(),
));
页面跳转使用iOS动画
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500), //动画时间为500毫秒
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
return new FadeTransition(
//使用渐隐渐入过渡,
opacity: animation,
child: PageB(), //路由B
);
},
),
);
渐隐渐入动画来实现路由过渡
Hero动画(共享元素动画)
// 路由A
class HeroAnimationRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.topCenter,
child: InkWell(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: ClipOval(
child: Image.asset("images/avatar.png",
width: 50.0,
),
),
),
onTap: () {
//打开B路由
Navigator.push(context, PageRouteBuilder(
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
return new FadeTransition(
opacity: animation,
child: Scaffold(
appBar: AppBar(
title: Text("原图"),
),
body: HeroAnimationRouteB(),
),
);
})
);
},
),
);
}
}
class HeroAnimationRouteB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: Image.asset("images/avatar.png"),
),
);
}
}
实现Hero动画只需要用Hero
组件将要共享的widget包装起来,并提供一个相同的tag即可,中间的过渡帧都是Flutter Framework自动完成的。必须要注意, 前后路由页的共享Hero
的tag必须是相同的,Flutter Framework内部正是通过tag来确定新旧路由页widget的对应关系的。
Hero动画的原理比较简单,Flutter Framework知道新旧路由页中共享元素的位置和大小,所以根据这两个端点,在动画执行过程中求出过渡时的插值(中间态)即可
交织动画
场景在不同阶段包含了多种动画,要实现这种效果,使用交织动画(Stagger Animation)会非常简单。交织动画需要注意以下几点:
- 要创建交织动画,需要使用多个动画对象(
Animation
)。 - 一个
AnimationController
控制所有的动画对象。 - 给每一个动画对象指定时间间隔(Interval)
class StaggerAnimation extends StatelessWidget {
final Animation<double> controller;
Animation<double> height;
Animation<EdgeInsets> padding;
Animation<Color> color;
StaggerAnimation({ Key key, this.controller }): super(key: key){
//高度动画
height = Tween<double>(
begin:.0 ,
end: 300.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.0, 0.6, //间隔,前60%的动画时间
curve: Curves.ease,
),
),
);
color = ColorTween(
begin:Colors.green ,
end:Colors.red,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.0, 0.6,//间隔,前60%的动画时间
curve: Curves.ease,
),
),
);
padding = Tween<EdgeInsets>(
begin:EdgeInsets.only(left: .0),
end:EdgeInsets.only(left: 100.0),
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.6, 1.0, //间隔,后40%的动画时间
curve: Curves.ease,
),
),
);
}
Widget _buildAnimation(BuildContext context, Widget child) {
return Container(
alignment: Alignment.bottomCenter,
padding:padding.value ,
child: Container(
color: color.value,
width: 50.0,
height: height.value,
),
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
builder: _buildAnimation,
animation: controller,
);
}
}
class StaggerRoute extends StatefulWidget {
@override
_StaggerRouteState createState() => _StaggerRouteState();
}
class _StaggerRouteState extends State<StaggerRoute> with TickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this
);
}
Future<Null> _playAnimation() async {
try {
//先正向执行动画
await _controller.forward().orCancel;
//再反向执行动画
await _controller.reverse().orCancel;
} on TickerCanceled {
// the animation got canceled, probably because we were disposed
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
_playAnimation();
},
child: Center(
child: Container(
width: 300.0,
height: 300.0,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1),
border: Border.all(
color: Colors.black.withOpacity(0.5),
),
),
//调用我们定义的交织动画Widget
child: StaggerAnimation(
controller: _controller
),
),
),
);
}
}
AnimatedSwitcher
AnimatedSwitcher
可以同时对其新、旧子元素添加显示、隐藏动画。
const AnimatedSwitcher({
Key key,
this.child,
@required this.duration, // 新child显示动画时长
this.reverseDuration,// 旧child隐藏的动画时长
this.switchInCurve = Curves.linear, // 新child显示的动画曲线
this.switchOutCurve = Curves.linear,// 旧child隐藏的动画曲线
this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // 动画构建器
this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, //布局构建器
})
当AnimatedSwitcher
的child发生变化时(类型或Key不同),旧child会执行隐藏动画,新child会执行执行显示动画。究竟执行何种动画效果则由transitionBuilder
参数决定,该参数接受一个AnimatedSwitcherTransitionBuilder
类型的builder
typedef AnimatedSwitcherTransitionBuilder =
Widget Function(Widget child, Animation<double> animation);
- 对旧child,绑定的动画会反向执行(reverse)
- 对新child,绑定的动画会正向指向(forward)
class SlideTransitionX extends AnimatedWidget {
SlideTransitionX({
Key key,
@required Animation<double> position,
this.transformHitTests = true,
this.direction = AxisDirection.down,
this.child,
})
: assert(position != null),
super(key: key, listenable: position) {
// 偏移在内部处理
switch (direction) {
case AxisDirection.up:
_tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
break;
case AxisDirection.right:
_tween = Tween(begin: Offset(-1, 0), end: Offset(0, 0));
break;
case AxisDirection.down:
_tween = Tween(begin: Offset(0, -1), end: Offset(0, 0));
break;
case AxisDirection.left:
_tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));
break;
}
}
Animation<double> get position => listenable;
final bool transformHitTests;
final Widget child;
//退场(出)方向
final AxisDirection direction;
Tween<Offset> _tween;
@override
Widget build(BuildContext context) {
Offset offset = _tween.evaluate(position);
if (position.status == AnimationStatus.reverse) {
switch (direction) {
case AxisDirection.up:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.right:
offset = Offset(-offset.dx, offset.dy);
break;
case AxisDirection.down:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.left:
offset = Offset(-offset.dx, offset.dy);
break;
}
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}