1 动画基础
不管是什么视图框架,动画的实现原理都是相同的,即在一段时限的时间内,多次、快速地改变视图外观来实现连续播放的效果。
视图的一次改变称为一个动画帧,对应一次屏幕刷新,决定动画流畅度的一个重要指标就是帧率(Frame Per Second,FPS),即每秒的动画帧数。
对于人眼,动画帧率超过16FPS就认为是流畅的,超过32FPS基本就感受不到任何卡顿,为了保证良好的视觉体验需要帧率尽可能达到60FPS。
Flutter框架是可以实现60FPS的,这和原生应用的帧率标准是基本持平的。
1.1 Animation
Animation是一个Flutter动画中的核心抽象类,主要用于保存动画的插值和状态,它本身与视图渲染没有任何关系。
Animation对象的状态有4种:
1)dismissed:动画处于开始状态;
2)forward:动画正在正向执行;
3)reverse:动画正在反向执行;
4)completed:动画处于结束状态。
Animation对象有Listeners和StatusListeners两个监听器两个监听器,可以用来监听动画的变化。
如果需要监听动画每一帧以及执行状态的变化,可以使用addListener()和addStatusListener()。
addListener()用于给Animation对象添加帧监听器,每一帧都会被调用,当帧监听器监听到状态发生改变后就会调用setState()来触发视图的重建。
addStatusListener()用于给Animation对象添加动画状态改变监听器,动画开始、结束、正向或反向时就会调用状态改变的监听器。
1.2 AnimationController
AnimationController表示动画控制器,是一个特殊的Animation对象,主要用于控制动画的开始、结束、正向、反向等操作。
1 2 3 4 5 6
| AnimationController controller = AnimationController ( duration: const Duration(milliseconds: 2000), lowerBound: 10.0, upperBound: 20.0, vsync: this, )
|
使用AnimationController 创建的Animation动画对象,默认情况下不会启动,要让动画运行,需要调用AnimationController 的forward()方法。
TickerProvider是一个抽象类:
1 2 3
| abstract class TickerProvider { Ticker createTicker(TickerCallback onTick); }
|
Flutter应用在启动时会绑定一个SchedulerBinding,通过SchedulerBinding可以给每一次屏幕刷新添加回调,而Ticker对象就是通过SchedulerBinding来实现屏幕刷新回调的,每次屏幕刷新都会调用TickerCallback 。
在Flutter动画中,使用Ticker而不是Timer来驱动动画,可以有效防止屏幕外动画(如锁屏)带来的资源消耗。Flutter在屏幕刷新时会通知绑定的SchedulerBinding,而Ticker是受SchedulerBinding驱动的,锁屏后屏幕停止刷新,Ticker也就不会再被触发。
AnimationController 常用函数:
1)forward():开始播放动画;
2)stop():停止动画播放;
3)reset():重置动画为初始化状态;
4)reverse():反向动画播放,必须正向动画播放完成后才有效;
5)repeat():循环播放动画;
6)dispose():销毁动画,释放动画占用资源。
1.3 Curve
Curve主要用来控制动画随时间的变化率,默认为均匀的线性变化。
在Flutter应用开发中,可以通过CurvedAnimation来指定动画的曲线:
1 2 3 4
| CurvedAnimation curve = CurvedAnimation ( parent: controller, curve: Curves.easeIn )
|
Curves类定义的动画曲线:
1)linear:匀速动画;
2)decelerate:匀减速动画;
3)ease:先加速后减速动画;
4)easeIn:先快后慢动画;
5)easeOut:先慢后快动画;
6)easeInOut:先慢,然后加速,最后减速动画。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import 'package:flutter/material.dart';
void main() => runApp(CurvePage());
class CurvePage extends StatefulWidget { State<StatefulWidget> createState() { return CurveState(); } }
class CurveState extends State<CurvePage> with SingleTickerProviderStateMixin{ CurvedAnimation curve; AnimationController controller;
@override void initState() { super.initState(); controller = AnimationController( duration: const Duration(milliseconds: 5000), vsync: this, ); curve = CurvedAnimation( parent: controller, curve: Curves.easeIn )..addListener(() { setState(() {}); }); controller.forward(); }
@override Widget build(BuildContext context) { return MaterialApp( title: '动画', home: Scaffold( appBar: AppBar(title: Text('动画')), body: Center( child: Container( margin: EdgeInsets.symmetric(vertical: 10), height: 300 * curve.value, width: 300 * curve.value, child: FlutterLogo() ) ) ) ); }
@override void dispose() { controller.dispose(); super.dispose(); } }
|
运行上面的代码,动画会在5秒内执行先慢后快的非线性动画,图标慢慢变大。
1.4 Tween
默认情况下,AnimationController创建的动画对象的取值范围是[0.0, 1.0],可以使用Tween来自定义范围。
1
| Tween doubleTween = Tween<double>(begin: 0.0, end: 100.0);
|
Tween是一个无状态对象,它继承自Animatable,Animatable是一个控制动画类型的类,定义了动画值的映射规则,需要传入begin和end两个参数。
Animatable支持多取值类型,如数字、颜色等。
1 2 3 4
| Tween colorTween = ColorTween( begin: Colors.white, end: Colors.black );
|
如果需要使用Tween对象,可以调用其animate(),然后传入一个控制器对象。
1 2 3 4 5 6 7 8 9
| AnimationController controller = AnimationController( duration: const Duration(milliseconds: 500), vsync: this );
Animation<int> alpha = IntTween( begin: 0, end: 255 ).animate(controller);
|
2 动画组件
2.1 基础用法
使用Animation和AnimationController实现心跳动画示例。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| import 'package:flutter/material.dart';
void main() => runApp(HeartAnimPage());
class HeartAnimPage extends StatefulWidget { HeartAnimPage({Key key}): super(key: key); _HeartAnimState createState() => _HeartAnimState(); }
class _HeartAnimState extends State<HeartAnimPage> with SingleTickerProviderStateMixin { AnimationController controller; Animation<double> animation;
@override void initState() { super.initState(); controller = AnimationController( duration: const Duration(seconds: 1), vsync: this ); animation = CurvedAnimation( parent: controller, curve: Curves.easeInOut, reverseCurve: Curves.easeOut ); animation.addStatusListener((status) { if (status == AnimationStatus.completed) { controller.reverse(); } else if (status == AnimationStatus.dismissed) { controller.forward(); } }); animation = Tween(begin: 50.0, end: 120.0).animate(controller); controller.forward(); }
@override Widget build(BuildContext context) { return MaterialApp( title: '动画--基础用法', home: Scaffold( appBar: AppBar(title: Text('动画--基础用法')), body: Center( child: AnimatedBuilder( animation: animation, builder: (ctx, child) { return Icon(Icons.favorite, color: Colors.red, size: animation.value); }, ) ) ) ); }
@override void dispose() { controller.dispose(); super.dispose(); } }
|
示例效果:
Flutter官方提供了AnimatedWidget组件,用于简化动画开发中addListener()和setState()的调用流程。
使用AnimatedWidget组件重构心跳动画的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| import 'package:flutter/material.dart';
void main() => runApp(HeartAnimPage());
class HeartAnimPage extends StatefulWidget { HeartAnimPage({Key key}): super(key: key); _HeartAnimState createState() => _HeartAnimState(); }
class _HeartAnimState extends State<HeartAnimPage> with SingleTickerProviderStateMixin { AnimationController controller; Animation<double> animation;
@override void initState() { super.initState(); controller = AnimationController( duration: const Duration(seconds: 1), vsync: this ); animation = CurvedAnimation( parent: controller, curve: Curves.easeInOut, reverseCurve: Curves.easeOut ); animation.addStatusListener((status) { if (status == AnimationStatus.completed) { controller.reverse(); } else if (status == AnimationStatus.dismissed) { controller.forward(); } }); animation = Tween(begin: 50.0, end: 120.0).animate(controller); controller.forward(); }
@override Widget build(BuildContext context) { return MaterialApp( title: '动画--AnimatedWidget', home: Scaffold( appBar: AppBar(title: Text('动画--AnimatedWidget')), body: Center( child: HeatAnimatedWidget(animation) ) ) ); }
@override void dispose() { controller.dispose(); super.dispose(); } }
class HeatAnimatedWidget extends AnimatedWidget { HeatAnimatedWidget(Animation animation): super(listenable: animation);
@override Widget build(BuildContext context) { Animation animation = listenable; return Icon(Icons.favorite, color: Colors.red, size: animation.value); } }
|
AnimatedWidget组件有效地将业务逻辑和功能组件分离,简化了动画操作的逻辑。
2.3 AnimatedBuilder
使用AnimatedBuilder组件, 系统只会重新构建动画组件自身,对父子组件不做任何处理,从而避免不必要的性能开销。
使用AnimatedBuilder组件还有以下优点:
1)不需要显示添加帧监听器以及调用setState();
2)缩小动画构建的范围,避免不必要的视图构建,从而提高视图渲染性能;
3)可以封装一些常见的动画效果,从而提高代码的复用性。
3 转场动画
在原生Android开发中,可以使用共享元素动画(Shared Element Transition,又称Hero Transition)来实现多个页面的切换动画。
Hero指的是可以在路由(即Flutter页面)之间飞行的组件。
在Flutter中,实现Hero动画效果至少需要两个路由,即源路由和目标路由,然后使用Hero组件包裹在需要动画控制的组件外面,同时为它们设置相同的tag属性。
Hero动画组件的构造函数:
1 2 3 4 5 6 7 8 9
| const Hero({ Key key, @required this.tag, this.createRectTween, this.flightShuttleBuilder, this.placeholderBuilder, this.transitionOnUserGestures = false, @required this.child, });
|
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| import 'package:flutter/material.dart';
void main() => runApp(HeroPage());
class HeroPage extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: '动画--转场动画', home: HeroAPage(), ); } }
class HeroAPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('第一个页面')), body: Container( child: Center( child: GestureDetector( child: Hero( tag: 'avatar', child: Image.asset( 'images/wx.png', width: 100, height: 100, fit: BoxFit.fill ), ), onTap: () { Navigator.push( context, MaterialPageRoute(builder: (BuildContext context) => HeroBPage()) ); } ) ) ) ); } }
class HeroBPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('第二个页面')), body: Container( child: Center( child: GestureDetector( child: Hero( tag: 'avatar', child: Image.asset( 'images/wx.png', width: 300, height: 300, fit: BoxFit.fill ), ), onTap: () { Navigator.pop(context); } ) ) ) ); } }
|
示例效果:
4 交错动画
在Flutter中,渐变、平移、缩放和旋转动画都属于基础动画,如果要实现一些复杂的动画效果,可以把这些基础动画组合起来形成一个动画序列或重叠动画,Flutter将这些动画序列或重叠动画称为交错动画。
在Flutter开发中,使用交错动画需要满足以下几点:
1)创建交错动画时需要创建多个动画对象;
2)一个AnimationController动画控制器控制所有的动画对象;
3)给每一个动画对象指定时间间隔,且动画的时间间隔为[0.0, 1.0]。
使用交错动画实现Flutter图标缩放和渐变的动画示例。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| import 'package:flutter/material.dart';
void main() => runApp(ParallelWidget());
class ParallelWidget extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: '动画--交错动画', home: ParallelPage(), ); } }
class ParallelPage extends StatefulWidget { @override State<StatefulWidget> createState() { return ParallelState(); } }
class ParallelState extends State<ParallelPage> with TickerProviderStateMixin { AnimationController controller; Animation<double> animation;
@override void initState() { super.initState(); controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 3000) ); animation = CurvedAnimation( parent: controller, curve: Curves.bounceIn ); animation.addStatusListener((status) { if(status == AnimationStatus.completed) { controller.reverse(); } else if (status == AnimationStatus.dismissed) { controller.forward(); } }); controller.forward(); }
@override Widget build(BuildContext context) { return ParallelAnimatedWidget(animation: animation); }
@override void dispose() { super.dispose(); controller.dispose(); } }
class ParallelAnimatedWidget extends AnimatedWidget { final _opacityTween = Tween<double>(begin: 0.1, end: 1.0); final _sizeTween = Tween<double>(begin: 0.0, end: 300.0);
ParallelAnimatedWidget({Key key, Animation<double> animation}): super(key: key, listenable: animation);
Widget build(BuildContext context) { Animation<double> animation = listenable; return Center( child: Opacity( opacity: _opacityTween.evaluate(animation), child: Container( margin: EdgeInsets.symmetric(vertical: 10.0), height: _sizeTween.evaluate(animation), width: _sizeTween.evaluate(animation), child: FlutterLogo() ), ), ); } }
|
示例效果: