概述
- 动画API认识
- 动画案例练习
- 其它动画补充
一、动画API认识
动画实际上是我们通过某些方式(某种对象,Animation对象)给Flutter引擎提供不同的值,而Flutter可以根据我们提供的值,给对应的小部件添加顺滑的动画效果。
-
1.1、Animation
在Flutter中,实现动画的核心类是动画,小部件可以直接将这些动画合并到自己的构建方法中来读取它们的当前值或监听其状态变化。
我们一起来看一下Animation
这个类,它是一个抽象类
:- addListener方法(
监听动画值的概念
)- 建立动画的状态值发生变化时,动画都会通知所有通过addListener添加的监听器。
- 通常,一个正在监听的动画的state对象会调用自身的setState方法,将自身本身作为这些监听器的插入函数来通知小部件,系统需要根据新状态值进行重新生成。
- addStatusListener(
监听动画状态的改变
)当动画的状态发生变化时,会通知所有通过addStatusListener添加的监听器。
通常情况下,动画会从dismissed状态开始,表示它处于变化区间的开始点。
举例来说,从0.0到1.0的动画在dismissed状态时的值应该是0.0。动画进行的下一状态可能是forward(例如从0.0到1.0)或者reverse(例如从1.0到0.0)。
-
最终,如果动画到达其区间的结束点(例如1.0),则动画会变成completed状态。
abstractclass Animation<T> extends Listenable implements ValueListenable<T> { const Animation(); // 添加动画监听器 @override void addListener(VoidCallback listener); // 移除动画监听器 @override void removeListener(VoidCallback listener); // 添加动画状态监听器 void addStatusListener(AnimationStatusListener listener); // 移除动画状态监听器 void removeStatusListener(AnimationStatusListener listener); // 获取动画当前状态 AnimationStatus get status; // 获取动画当前的值 @override T get value; }
- addListener方法(
-
1.2、AnimationController
Animation是一个抽象类,并不能直接创建对象实现动画的使用。
AnimationController是Animation的一个子类
,实现动画通常我们需要创建AnimationController
对象。- AnimationController会生成一系列的值,交替情况下值是0.0到1.0区间的值;
除了上面的监听器,获取动画的状态,值之外,AnimationController还提供了对动画的控制:
- forward:向前执行动画
- 反向:方向播放动画
- stop:停止动画
AnimationController的源码:
class AnimationController extends Animation<double> with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin { AnimationController({ // 初始化值 double value, // 动画执行的时间 this.duration, // 反向动画执行的时间 this.reverseDuration, // 最小值 this.lowerBound = 0.0, // 最大值 this.upperBound = 1.0, // 刷新率ticker的回调(看下面详细解析) @required TickerProvider vsync, }) }
- AnimationController有一个必传的参数
vsync
,它是什么呢?- Flutter的渲染闭环,Flutter每次渲染一帧画面之前都需要等待一个vsync信号。
- 这里也是为了监听vsync信号,当Flutter开发的应用程序不再接受同步信号时(比如锁屏或退到后台),那么继续执行动画会消耗性能。
- 这个时候我们设置了Ticker,就不会再出发动画了。
- 开发中比较常见的是将
SingleTickerProviderStateMixin
混入到State的定义中。
-
1.3、CurvedAnimation(设置动画执行的速率-速率曲线)
CurvedAnimation也是Animation的一个实现类,它的目的是为了给AnimationController增加动画曲线:
CurvedAnimation可以将AnimationController
和Curve
结合起来,生成一个新的Animation对象class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> { CurvedAnimation({ // 通常传入一个AnimationController @requiredthis.parent, // Curve类型的对象 @requiredthis.curve, this.reverseCurve, }); }
- Curve类型的对象的有一些常量Curves(和Color类型有一些Colors是一样的),可以供我们直接使用:
- 对应值的效果,可以直接查看官网(有对应的gif效果,一目了然)
- https://api.flutter.dev/flutter/animation/Curves-class.html
官方也发表了自己的定义Curse的一个示例
import'dart:math'; class ShakeCurve extends Curve { @override double transform(double t) => sin(t * pi * 2); }
- Curve类型的对象的有一些常量Curves(和Color类型有一些Colors是一样的),可以供我们直接使用:
-
1.4、Tween
默认情况下,AnimationController动画生成的值所在区间是0.0到1.0
如果希望使用这个以外的值,或者其他的数据类型,就需要使用Tween
Tween的源码:源码非常简单,预设两个值即可,可以定义一个范围。class Tween<T extends dynamic> extends Animatable<T> { // begin 开始值,end 结束值 Tween({ this.begin, this.end }); }
Tween也有一些子类,比如ColorTween、BorderTween,可以针对动画或者边框来设置动画的值。
Tween.animate
要使用Tween对象,需要调用Tween的animate()
方法,传入一个Animation对象。
二、动画案例练习
-
2.1. 动画的基本使用(
不可取,优缺点
)
我们来完成一个案例:点击案例后执行一个心跳动画,可以反复执行
-
再次点击可以暂停和重新开始动画
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, splashColor: Colors.transparent), home: HYHomePage(), ); } } class HYHomePage extends StatefulWidget { @override _HYHomePageState createState() => _HYHomePageState(); } class _HYHomePageState extends State<HYHomePage> with SingleTickerProviderStateMixin { // 创建AnimationController AnimationController _controller; Animation _animation; Animation _sizeAnim; @override void initState() { super.initState(); // 1.创建AnimationController _controller = AnimationController( vsync: this, duration: Duration(seconds: 2) ); // 2.动画添加Curve效果 _animation = CurvedAnimation(parent: _controller, curve: Curves.linear); // 3.Tween 设置值的范围 _sizeAnim = Tween(begin: 50.0, end: 150.0).animate(_animation); // 4.监听动画值的改变 _controller.addListener(() { setState(() {}); }); // 5.监听动画的状态改变 _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); } else if (status == AnimationStatus.dismissed) { _controller.forward(); } }); } @override Widget build(BuildContext context) { print("执行_HYHomePageState的build方法"); return Scaffold( appBar: AppBar( title: Text("首页"), ), body: return Center( child: Icon(Icons.favorite, color: Colors.red, size: _sizeAnim.value,), ); floatingActionButton: FloatingActionButton( child: Icon(Icons.play_arrow), onPressed: () { if (_controller.isAnimating) { _controller.stop(); print(_controller.status); } else if (_controller.status == AnimationStatus.forward) { _controller.forward(); } else if (_controller.status == AnimationStatus.reverse) { _controller.reverse(); } else { _controller.forward(); } }, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
-
2.2、AnimatedWidget(
不可取,优缺点
)
在上面的代码中,我们必须监听动画值的改变,并且改变后需要调用setState(也就是上面的第4步),这会带来两个问题:- 1.执行动画必须包含这部分代码,代码比较冗余
- 2.调用setState意味着整个State类中的build方法就会被重新build
如何可以优化上面的操作:创建一个Widget继承自AnimatedWidget:
class IconAnimation extends AnimatedWidget { IconAnimation(Animation animation): super(listenable: animation); @override Widget build(BuildContext context) { Animation animation = listenable; return Icon(Icons.favorite, color: Colors.red, size: animation.value,); } }
那么2.1中的 的 第四步就可以去掉了,在Icon调用的地方直接:
IconAnimation(_animation)
- 缺点是:1、每次都需要创建一个类,类里面的build也会打印;2、如果创建的Widget有子类,那么子类依然会重复的build
-
2.3、
AnimatedBuilder
(优解)
AnimatedBuilder 可以解决上面 AnimatedWidget 产生的两个问题,代码如下class _HYHomePageState extends State<HYHomePage> with SingleTickerProviderStateMixin { // 创建AnimationController AnimationController _controller; Animation _animation; @override void initState() { super.initState(); // 1.创建AnimationController _controller = AnimationController( vsync: this, duration: Duration(seconds: 2) ); // 2.设置Curve的值 _animation = CurvedAnimation(parent: _controller, curve: Curves.linear); // 3.Tween _animation = Tween(begin: 50.0, end: 150.0).animate(_animation); // 监听动画的状态改变 _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); } else if (status == AnimationStatus.dismissed) { _controller.forward(); } }); } @override Widget build(BuildContext context) { print("执行_HYHomePageState的build方法"); return Scaffold( appBar: AppBar( title: Text("首页"), ), body: Center( child: AnimatedBuilder( animation: _controller, builder: (ctx, child) { return Icon(Icons.favorite, color: Colors.red, size: _animation.value,); }, ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.play_arrow), onPressed: () { if (_controller.isAnimating) { _controller.stop(); print(_controller.status); } else if (_controller.status == AnimationStatus.forward) { _controller.forward(); } else if (_controller.status == AnimationStatus.reverse) { _controller.reverse(); } else { _controller.forward(); } }, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
三、其它动画补充
-
3.1、交织动画(多个动画同时执行)、
案例说明:点击floatingActionButton执行动画
动画集合了透明度变化
、大小变化
、颜色变化
、旋转动画
等;
我们这里是通过多个Tween生成了多个Animation对象;
代码如下class HYHomePage extends StatefulWidget { @override _HYHomePageState createState() => _HYHomePageState(); } class _HYHomePageState extends State<HYHomePage> with SingleTickerProviderStateMixin { // 创建AnimationController AnimationController _controller; Animation _animation; // 大小 Animation<double> _sizeAnim; // 颜色 Animation _colorAnim; // 透明度 Animation<double> _opactiyAnim; // 角度 Animation<double> _radiansAnim; @override void initState() { super.initState(); // 1.创建AnimationController _controller = AnimationController( vsync: this, duration: Duration(seconds: 2) ); // 2.设置Curve的值 _animation = CurvedAnimation(parent: _controller, curve: Curves.linear); // 3.Tween _sizeAnim = Tween(begin: 10.0, end: 150.0).animate(_controller); _colorAnim = ColorTween(begin: Colors.brown, end: Colors.green).animate(_controller); _opactiyAnim = Tween(begin: 0.0, end: 1.0).animate(_controller); _radiansAnim = Tween(begin: 0.0, end: 2 * pi).animate(_controller); // 监听动画的状态改变 _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); } else if (status == AnimationStatus.dismissed) { _controller.forward(); } }); } @override Widget build(BuildContext context) { print("执行_HYHomePageState的build方法"); return Scaffold( appBar: AppBar( title: Text("首页"), ), body: Center( child: AnimatedBuilder( animation: _controller, builder: (ctx, child) { return Opacity( opacity: _opactiyAnim.value, child: Transform( transform: Matrix4.rotationZ(_radiansAnim.value), alignment: Alignment.center, child: Container( width: _sizeAnim.value, height: _sizeAnim.value, color: _colorAnim.value, ), ), ); }, ) ), floatingActionButton: FloatingActionButton( child: Icon(Icons.play_arrow), onPressed: () { if (_controller.isAnimating) { _controller.stop(); print(_controller.status); } else if (_controller.status == AnimationStatus.forward) { _controller.forward(); } else if (_controller.status == AnimationStatus.reverse) { _controller.reverse(); } else { _controller.forward(); } }, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
-
3.2、Hero动画
移动端开发会经常遇到类似这样的需求:- 点击一个头像,显示头像的大图,并且从原来图像的Rect到大图的Rect
- 点击一个商品的图片,可以展示商品的大图,并且从原来图像的Rect到大图的Rect
这种跨页面共享的动画被称之为享元动画(Shared Element Transition)
在Flutter中,有一个专门的Widget可以来实现这种动画效果:Hero
实现Hero动画,需要如下步骤:- 1.在第一个Page1中,定义一个起始的Hero Widget,被称之为source hero,并且绑定一个tag;
- 2.在第二个Page2中,定义一个终点的Hero Widget,被称之为 destination hero,并且绑定相同的tag;
- 3.可以通过Navigator来实现第一个页面Page1到第二个页面Page2的跳转过程;
Flutter会设置Tween来界定Hero从起点到终端的大小和位置,并且在图层上执行动画效果。
首页Page的核心代码:GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 6, mainAxisSpacing: 6, childAspectRatio: 16/9 ), children: List.generate(20, (index) { String imageURL = "https://picsum.photos/200/300?random=$index"; return GestureDetector( onTap: () { Navigator.of(context).push(PageRouteBuilder( pageBuilder: (ctx, animation1, animation2) { return FadeTransition( opacity: animation1, child: JKImageDeyail(imageURL), ); }, )); }, child: Hero(tag: imageURL, child: Image.network(imageURL, fit: BoxFit.cover,)), ); }), ),
提示:外层包裹了一个:手势
GestureDetector
,跳转用的带动画的 PageRouteBuilder,对于跳转的页面包裹了渐变 FadeTransition
对于展示的 Image 我们包裹了一个 Hero ,对于 Hero 下个页面也要有 Hero,并且和当前的 Hero 的 tag 保持一致图片展示Page
import 'package:flutter/material.dart'; class JKImageDeyail extends StatelessWidget { final String _imageUrl; JKImageDeyail(this._imageUrl); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( title: Text('图片详情'), ), body: Center( child: GestureDetector( onTap: () { Navigator.of(context).pop(); }, child: Hero( tag: _imageUrl, child: Image.network( _imageUrl, width: double.infinity, fit: BoxFit.cover, ), ), ), ), ); } }