📚 目录
- 介绍
- 介绍
- Animation
- Curve
- AnimationController
- Tween
- 监听动画
- 自定义路由切换动画
- Hero飞行动画
- 交织动画
- 动画切换组件
- AnimatedSwitcher
- AnimatedSwitcher封装
- 动画过渡组件
本文学习和引用自《Flutter实战·第二版》:作者:杜文
1. 介绍
在任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画。我们将UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS,即每秒的动画帧数。帧率越高则动画就会越流畅!一般情况下,对于人眼来说,动画帧率超过16 FPS,就基本能看了,超过 32 FPS就会感觉相对平滑,而超过 32 FPS,大多数人基本上就感受不到差别了。由于动画的每一帧都是要改变UI输出,是比较耗资源的,而在Flutter中,理想情况下是可以实现 60FPS 的,这和原生应用能达到的帧率是基本是持平的。
2. Flutter中动画抽象
为了方便开发者创建动画,不同的UI系统对动画都进行了一些抽象,比如在 Android 中可以通过XML来描述一个动画然后设置给View。Flutter中也对动画进行了抽象,主要涉及 Animation、Curve、Controller、Tween这四个角色,它们一起配合来完成一个完整动画
2-1. Animation
Animation是一个抽象类,它本身和UI渲染没有任何关系,它主要的功能是保存动画的插值和状态。Animation对象是一个在一段时间内依次生成一个区间(Tween)之间值的类。Animation对象在整个动画执行过程中输出的值可以是线性的、曲线的、一个步进函数或者任何其他曲线函数等等,这由Curve来决定。 根据Animation对象的控制方式,动画可以正向运行(从起始状态开始,到终止状态结束),也可以反向运行,甚至可以在中间切换方向。我们可以通过Animation来监听动画每一帧以及执行状态的变化:
-
addListener():用于给Animation添加帧监听器,在每一帧都会被调用。帧监听器中最常见的行为是改变状态后调用setState()来触发UI重建。
-
addStatusListener():给Animation添加“动画状态改变”监听器;动画开始、结束、正向或反向时会调用状态改变的监听器。
2-2. Curve
动画过程可以是匀速的、匀加速的或者先加速后减速等。Flutter中通过Curve(曲线)来描述动画过程,我们把匀速动画称为线性的,而非匀速动画称为非线性的。
曲线 | 动画过程 |
---|---|
linear | 匀速的 |
decelerate | 匀减速 |
ease | 开始加速,后面减速 |
easeIn | 开始慢,后面快 |
easeOut | 开始快,后面慢 |
easeInOut | 开始慢,然后加速,最后再减速 |
- 指定动画曲线
final CurvedAnimation curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
- 定义一个正弦曲线
class ShakeCurve extends Curve { double transform(double t) {return math.sin(t * math.PI * 2);}
}
2-3. AnimationController
AnimationController用于控制动画,它包含动画的启动forward()、停止stop() 、反向播放 reverse()等方法。AnimationController会在动画的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内线性的生成从 0.0 到1.0(默认区间)的数字。
- 创建animation对象
final AnimationController controller = AnimationController(// 动画时长duration: const Duration(milliseconds: 2000),// 指定生成数字的区间lowerBound: 10.0,upperBound: 20.0,// 绑定State对象vsync: this
);
2-4. Tween
默认情况下,AnimationController对象值的范围是[0.0,1.0]。如果我们需要构建UI的动画值在不同的范围或不同的数据类型,则可以使用Tween来添加映射以生成不同的范围或数据类型的值。
final Tween doubleTween = Tween<double>(begin: -200.0, end: 0.0);
2-5. 监听动画
我们可以通过Animation的addStatusListener()方法来添加动画状态改变监听器。Flutter中,有四种动画状态,在AnimationStatus枚举类中定义。
属性值 | 描述 |
---|---|
dismissed | 动画在起始点停止 |
forward | 动画正在正向执行 |
reverse | 动画正在反向执行 |
completed | 动画在终点停止 |
- 完整动画例子:
import 'package:flutter/material.dart';/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现 需要继承TickerProvider,如果有多个AnimationController,则应该继承TickerProviderStateMixin
class HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {late Animation<double> animation;late AnimationController controller;void initState() {super.initState();controller = AnimationController(duration: const Duration(seconds: 3),vsync: this);// 使用弹性曲线animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);// 匀速图片宽高从0变到300animation = Tween(begin: 0.0, end: 300.0).animate(controller)..addListener(() {setState(() => {});});animation.addStatusListener((status) {if (status == AnimationStatus.completed) {// 动画执行结束时反向执行动画controller.reverse();} else if (status == AnimationStatus.dismissed) {// 动画恢复到初始状态时执行动画(正向)controller.forward();}});// 启动动画(正向执行)controller.forward();}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Container(alignment: Alignment.center,child: Image.asset('static/portrait.png',width: animation.value,height: animation.value),));}void dispose() {controller.dispose();super.dispose();}
}
3. 自定义路由切换动画
Material组件库中提供了一个MaterialPageRoute组件,它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换。现在,我们如果在Android上也想使用左右切换风格,该怎么做?一个简单的作法是可以直接使用CupertinoPageRoute。
Navigator.push(context, CupertinoPageRoute( builder: (context)=>PageB(),));
- 使用PageRouteBuilder来自定义路由切换动画
import 'package:flutter/material.dart';
import 'package:demo1/views/login/view.dart';/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现
class HomePageState extends State<HomePage> {Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Container(alignment: Alignment.center,child: Image.asset('static/portrait.png',width: 200.0,height: 200.0),),floatingActionButton: FloatingActionButton(onPressed: () {Navigator.push(context,PageRouteBuilder(transitionDuration: const Duration(milliseconds: 500),pageBuilder: (BuildContext context, Animation<double> animation,Animation secondaryAnimation) {return FadeTransition(// 使用渐隐渐入过渡,opacity: animation,// 其他页面child: const LoginPage(),);},),);},child: const Text('跳'),),);}
}
4. Hero飞行动画
在Flutter中将图片从一个路由“飞”到另一个路由称为hero动画,尽管相同的动作有时也称为 共享元素转换。实现 Hero 动画只需要用Hero组件将要共享的 widget 包装起来,并提供一个相同的 tag 即可。
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
import 'package:demo1/views/login/view.dart';/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现
class HomePageState extends State<HomePage> {Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Container(alignment: Alignment.center,child: InkWell(onTap: () {Navigator.push(context, PageRouteBuilder(pageBuilder: (BuildContext context,animation,secondaryAnimation,) {return FadeTransition(opacity: animation,child: Scaffold(appBar: AppBar(title: const Text("原图"),),body: HeroAnimationB(),),);},));},child: Hero(tag: 'xxxxx',child: ClipOval(child: Image.asset('static/portrait.png', width: 50.0),),),),));}
}/// 大图
class HeroAnimationB extends StatelessWidget {Widget build(BuildContext context) {return Center(child: Hero(tag: 'xxxxx',child: Image.asset('static/portrait.png'),),);}
}
5. 交织动画
有些时候我们可能会需要一些复杂的动画,这些动画可能由一个动画序列或重叠的动画组成。要实现这种效果,使用交织动画比较简单。要创建交织动画,需要使用多个动画对象,并且使用一个AnimationController控制所有的动画对象,给每一个动画对象指定时间间隔。
import 'package:flutter/material.dart';/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现
class HomePageState extends State<HomePage> with TickerProviderStateMixin {late AnimationController myController;void initState() {super.initState();myController = AnimationController(duration: const Duration(milliseconds: 2000),vsync: this,);}handlePlayAnimation() async {try {//先正向执行动画await myController.forward().orCancel;//再反向执行动画await myController.reverse().orCancel;} on TickerCanceled {//捕获异常。可能发生在组件销毁时,计时器会被取消。}}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Container(alignment: Alignment.center,child: Column(children: [ElevatedButton(onPressed: () => handlePlayAnimation(),child: const Text("Start Animation"),),Container(width: 300.0,height: 300.0,decoration: BoxDecoration(color: Colors.black.withOpacity(0.1),border: Border.all(color: Colors.black.withOpacity(0.5),),),//调用我们定义的交错动画Widgetchild: StaggerAnimation(controller: myController),),],),));}
}/// 动画组件
class StaggerAnimation extends StatelessWidget {StaggerAnimation({Key? key,required this.controller,}) : super(key: key) {// 高度动画height = Tween<double>(begin: .0,end: 300.0,).animate(CurvedAnimation(parent: controller,curve: const Interval(0.0, 0.6, //间隔,前60%的动画时间curve: Curves.ease,),),);// 颜色动画color = ColorTween(begin: Colors.green,end: Colors.red,).animate(CurvedAnimation(parent: controller,curve: const Interval(0.0, 0.6, //间隔,前60%的动画时间curve: Curves.ease,),),);// 偏移动画padding = Tween<EdgeInsets>(begin: const EdgeInsets.only(left: .0),end: const EdgeInsets.only(left: 100.0),).animate(CurvedAnimation(parent: controller,curve: const Interval(0.6, 1.0, //间隔,后40%的动画时间curve: Curves.ease,),),);}late final Animation<double> controller;late final Animation<double> height;late final Animation<EdgeInsets> padding;late final Animation<Color?> color;Widget handleBuildAnimation(BuildContext context, child) {return Container(alignment: Alignment.bottomCenter,padding: padding.value,child: Container(color: color.value,width: 50.0,height: height.value,),);}Widget build(BuildContext context) {return AnimatedBuilder(builder: handleBuildAnimation,animation: controller,);}
}
6. 动画切换组件
开发中,我们经常会遇到切换UI元素的场景,比如Tab切换、路由切换。为了增强用户体验,通常在切换时都会指定一个动画,以使切换过程显得平滑。Flutter SDK中提供了一个AnimatedSwitcher组件,它定义了一种通用的UI切换抽象。
6-1. AnimatedSwitcher
AnimatedSwitcher 可以同时对其新、旧子元素添加显示、隐藏动画。也就是说在AnimatedSwitcher的子元素发生变化时,会对其旧元素和新元素做动画。当AnimatedSwitcher的 child 发生变化时(类型或 Key 不同),旧 child 会执行隐藏动画,新 child 会执行执行显示动画。默认情况,AnimatedSwitcher会对新旧child执行“渐隐”和“渐显”动画。
import 'package:flutter/material.dart';/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现
class HomePageState extends State<HomePage> {int myCount = 0;Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Container(alignment: Alignment.center,child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [AnimatedSwitcher(duration: const Duration(milliseconds: 500),transitionBuilder: (Widget child, Animation<double> animation) {// 执行缩放动画return ScaleTransition(scale: animation, child: child);},child: Text('$myCount',// 显示指定key,不同的key会被认为是不同的Text,这样才能执行动画key: ValueKey<int>(myCount),style: Theme.of(context).textTheme.headline4,),),ElevatedButton(child: const Text('+1'),onPressed: () {setState(() {myCount += 1;});},)],),));}
}
6-2. AnimatedSwitcher封装
封装一个通用的SlideTransitionX 来实现左出右入,上入下出等 出入动画。修改direction的值即可修改方向。
import 'package:flutter/material.dart';
/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现
class HomePageState extends State<HomePage> {int myCount = 0;Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Container(alignment: Alignment.center,child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [AnimatedSwitcher(duration: const Duration(milliseconds: 500),transitionBuilder: (Widget child, Animation<double> animation) {// 执行缩放动画return SlideTransitionX(direction: AxisDirection.down, // 上入下出position: animation,child: child);},child: Text('$myCount',// 显示指定key,不同的key会被认为是不同的Text,这样才能执行动画key: ValueKey<int>(myCount),style: Theme.of(context).textTheme.headline4,),),ElevatedButton(child: const Text('+1'),onPressed: () {setState(() {myCount += 1;});},)],),));}
}/// 封装的动画容器
class SlideTransitionX extends AnimatedWidget {SlideTransitionX({Key? key,required Animation<double> position,this.transformHitTests = true,this.direction = AxisDirection.down,required this.child,}) : super(key: key, listenable: position) {switch (direction) {case AxisDirection.up:_tween = Tween(begin: const Offset(0, 1), end: const Offset(0, 0));break;case AxisDirection.right:_tween = Tween(begin: const Offset(-1, 0), end: const Offset(0, 0));break;case AxisDirection.down:_tween = Tween(begin: const Offset(0, -1), end: const Offset(0, 0));break;case AxisDirection.left:_tween = Tween(begin: const Offset(1, 0), end: const Offset(0, 0));break;}}final bool transformHitTests;final Widget child;final AxisDirection direction;late final Tween<Offset> _tween;Widget build(BuildContext context) {final position = listenable as Animation<double>;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,);}
}
7. 动画过渡组件
在Widget属性发生变化时会执行过渡动画的组件统称为”动画过渡组件“,而动画过渡组件最明显的一个特征就是它会在内部自管理AnimationController。而为了方便使用者可以自定义动画的曲线、执行时长、方向等,通常都需要使用者自己提供一个AnimationController对象来自定义这些属性值。如此一来,使用者就必须得手动管理AnimationController,这又会增加使用的复杂性。因此,如果也能将AnimationController进行封装,则会大大提高动画组件的易用性。Flutter SDK中也预置了很多动画过渡组件,如下:
组件名 | 功能 |
---|---|
AnimatedPadding | 在padding发生变化时会执行过渡动画到新状态 |
AnimatedPositioned | 配合Stack一起使用,当定位状态发生变化时会执行过渡动画到新的状态。 |
AnimatedOpacity | 在透明度opacity发生变化时执行过渡动画到新状态 |
AnimatedAlign | 当alignment发生变化时会执行过渡动画到新的状态 |
AnimatedContainer | 当Container属性发生变化时会执行过渡动画到新的状态 |
AnimatedDefaultTextStyle | 当字体样式发生变化时,子组件中继承了该样式的文本组件会动态过渡到新样式 |
import 'package:flutter/material.dart';/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现
class HomePageState extends State<HomePage> {double myHeight = 100;Color myColor = Colors.red;Widget build(BuildContext context) {var duration = const Duration(milliseconds: 400);return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Container(alignment: Alignment.center,child: AnimatedContainer(duration: duration,height: myHeight,color: myColor,child: TextButton(onPressed: () {if (myHeight < 300) {setState(() {myHeight = 300;myColor = Colors.blue;});} else {setState(() {myHeight = 100;myColor = Colors.red;});}},child: const Text('点击执行变化',style: TextStyle(color: Colors.white),))),));}
}
本次分享就到这儿啦,我是鹏多多,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~
往期文章
- 手把手教你搭建规范的团队vue项目,包含commitlint,eslint,prettier,husky,commitizen等等
- Web Woeker和Shared Worker的使用以及案例
- Vue2全家桶+Element搭建的PC端在线音乐网站
- vue3+element-plus配置cdn
- 助你上手Vue3全家桶之Vue3教程
- 助你上手Vue3全家桶之VueX4教程
- 助你上手Vue3全家桶之Vue-Router4教程
- 超详细!Vue的九种通信方式
- 超详细!Vuex手把手教程
- 使用nvm管理node.js版本以及更换npm淘宝镜像源
- vue中利用.env文件存储全局环境变量,以及配置vue启动和打包命令
- 超详细!Vue-Router手把手教程
个人主页
- CSDN
- GitHub
- 简书
- 博客园
- 掘金