在上一篇文章Flutter动画学习之简介中了解了Animation、Curve、Controller、Tween在Flutter中动画中最主要的四个角色。本篇文章就开始实践。
本篇文章Demo下载
基础匀速版
创建一个AnimationController,指定时间3秒。使用Tween指定范围100到300。通过controller.forward()启动动画,通过controller.reset()重置动画可重新再次启动动画。
scss
///线性缩放大小
class ScaleAnimationDemo1 extends StatefulWidget {
const ScaleAnimationDemo1({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ScaleAnimationDemoState1();
}
//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationDemoState1 extends State<ScaleAnimationDemo1>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 3), vsync: this);
//没有指定Curve,过程是线性的,从100变到300
animation = Tween(begin: 100.0, end: 300.0).animate(controller)
..addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
const Padding(padding: EdgeInsets.only(top: 120)),
TextButton(
child: const Text(
'线性缩放大小',
style: TextStyle(color: Colors.blueAccent),
),
onPressed: () => setState(() {
//重置动画
controller.reset();
//启动动画(正向执行)
controller.forward();
}),
),
Icon(Icons.access_alarm, size: animation.value)
],
);
}
@override
void dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
addListener()函数调用了setState(),所以每次动画生成一个新的数字时,当前帧被标记为脏(dirty),这会导致widget的build()方法再次被调用,而在build()中,Icon的size使用的是animation.value ,所以就会逐渐放大。值得注意的是动画完成时要释放控制器(调用dispose()方法)以防止内存泄漏。
Curve曲线版
上述例子由于没有指定Curve,所以放大的过程是线性的(匀速),下面指定一个Curve,来实现一个类似于弹簧效果的动画过程。
需要使用CurvedAnimation包装AnimationController和Curve生成一个新的动画对象。
scss
///弹簧效果Curve缩放大小
class ScaleAnimationDemo2 extends StatefulWidget {
const ScaleAnimationDemo2({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ScaleAnimationDemoState2();
}
//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationDemoState2 extends State<ScaleAnimationDemo2>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 3), vsync: this);
//指定弹簧效果Curve
animation = CurvedAnimation(parent: controller, curve: Curves.bounceOut);
animation = Tween(begin: 100.0, end: 300.0).animate(animation)
..addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
const Padding(padding: EdgeInsets.only(top: 120)),
TextButton(
child: const Text(
'弹簧效果Curve缩放大小',
style: TextStyle(color: Colors.blueAccent),
),
onPressed: () => setState(() {
//重置动画
controller.reset();
//启动动画(正向执行)
controller.forward();
}),
),
Icon(Icons.access_alarm, size: animation.value)
],
);
}
@override
void dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
使用AnimatedWidget简化
可以发现更新UI都是通过addListener()和setState(),所有的动画都需要如此属实是重复性工作了。
AnimatedWidget类封装了调用setState()的细节,并允许将widget分离出来。利用 AnimatedWidget 创建一个可以重复使用运行动画的widget。
scala
///AnimatedWidget类封装了调用setState()的细节,并允许将 widget 分离出来
class ScaleAnimationWidget extends AnimatedWidget {
const ScaleAnimationWidget({Key? key, required Animation<double> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Icon(Icons.access_alarm, size: animation.value);
}
}
class ScaleAnimationDemo3 extends StatefulWidget {
const ScaleAnimationDemo3({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ScaleAnimationDemoState3();
}
//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationDemoState3 extends State<ScaleAnimationDemo3>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 3), vsync: this);
//指定弹簧效果Curve
animation = CurvedAnimation(parent: controller, curve: Curves.bounceOut);
animation = Tween(begin: 100.0, end: 300.0).animate(animation);
}
@override
Widget build(BuildContext context) {
return
Column(
children: [
const Padding(padding: EdgeInsets.only(top: 120)),
TextButton(
child: const Text(
'弹簧效果Curve缩放大小',
style: TextStyle(color: Colors.blueAccent),
),
onPressed: () => setState(() {
//重置动画
controller.reset();
//启动动画(正向执行)
controller.forward();
}),
),
ScaleAnimationWidget(animation: animation)
],
);
}
@override
void dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
Flutter API 中的 AnimatedWidget:PositionedTransition, RotationTransition, ScaleTransition, SizeTransition, SlideTransition等。
动画状态监听
通过Animation的addStatusListener()方法来添加动画状态改变监听器。 Flutter中,有四种动画状态,在AnimationStatus枚举类中定义。
arduino
/// The status of an animation.
enum AnimationStatus {
/// The animation is stopped at the beginning.
dismissed,
/// The animation is running from beginning to end.
forward,
/// The animation is running backwards, from end to beginning.
reverse,
/// The animation is stopped at the end.
completed,
}
在上述例子上,通过监听动画状态,当动画执行结束时反向执行动画,当动画恢复到初始状态时正向执行动画:
scss
class ScaleAnimationDemo4 extends StatefulWidget {
const ScaleAnimationDemo4({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ScaleAnimationDemoState4();
}
//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationDemoState4 extends State<ScaleAnimationDemo4>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 3), vsync: this);
animation = Tween(begin: 100.0, end: 300.0).animate(controller)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
}
@override
Widget build(BuildContext context) {
return
Column(
children: [
const Padding(padding: EdgeInsets.only(top: 120)),
TextButton(
child: const Text(
'缩放大小',
style: TextStyle(color: Colors.blueAccent),
),
onPressed: () => setState(() {
//启动动画(正向执行)
controller.forward();
}),
),
ScaleAnimationWidget(animation: animation)
],
);
}
@override
void dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
使用AnimatedBuilder重构
使用AnimatedWidget可以从动画中分离出 widget,而动画的渲染过程(即设置宽高)仍然在AnimatedWidget 中。
而AnimatedBuilder正是将渲染逻辑分离出来。AnimatedBuilder知道如何渲染过渡效果,但AnimatedBuilder不会渲染 widget,也不会控制动画对象。使用 AnimatedBuilder描述一个动画是其他 widget 构建方法的一部分。
AnimatedBuilder 作为渲染树的一个单独类。像 AnimatedWidget,AnimatedBuilder 自动监听动画对象提示,并在必要时在 widget 树中标出,所以这时不需要调用 addListener()。
scss
class ScaleAnimationDemo5 extends StatefulWidget {
const ScaleAnimationDemo5({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ScaleAnimationDemoState5();
}
//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationDemoState5 extends State<ScaleAnimationDemo5>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 3), vsync: this);
animation = Tween(begin: 100.0, end: 300.0).animate(controller);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
const Padding(padding: EdgeInsets.only(top: 120)),
TextButton(
child: const Text(
'缩放大小',
style: TextStyle(color: Colors.blueAccent),
),
onPressed: () => setState(() {
//重置动画
controller.reset();
//启动动画(正向执行)
controller.forward();
}),
),
AnimatedBuilder(
animation: animation,
builder: (BuildContext ctx, child) {
return Icon(Icons.access_alarm, size: animation.value);
},
)
],
);
}
@override
void dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
Flutter API 中 AnimatedBuilders:BottomSheet, ExpansionTile, PopupMenu, ProgressIndicator, RefreshIndicator, Scaffold, SnackBar, TabBar, TextField等。
复合补间动画
在同一个动画控制器中使用复合补间动画可以达到多个动画效果,每个补间动画控制一个动画的不同效果。 由于AnimatedWidget和AnimatedBuilder都只能读取单一的 Animation 对象,因此每一个动画效果都创建一个Tween对象并计算确切值Tween.evaluate()。
scss
class ScaleAnimationDemo6 extends StatefulWidget {
const ScaleAnimationDemo6({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _ScaleAnimationDemoState6();
}
//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationDemoState6 extends State<ScaleAnimationDemo6>
with SingleTickerProviderStateMixin {
late Tween<double> sizeTween;
late Tween<double> opacityTween;
late AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 3), vsync: this);
sizeTween = Tween(begin: 100.0, end: 300.0);
opacityTween = Tween(begin: 0.1, end: 1.0);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
const Padding(padding: EdgeInsets.only(top: 120)),
TextButton(
child: const Text(
'缩放大小同时透明度增加',
style: TextStyle(color: Colors.blueAccent),
),
onPressed: () => setState(() {
//重置动画
controller.reset();
//启动动画(正向执行)
controller.forward();
}),
),
AnimatedBuilder(
animation: controller,
builder: (BuildContext ctx, child) {
return Opacity(
opacity: opacityTween.evaluate(controller),
child: Icon(Icons.access_alarm,
size: sizeTween.evaluate(controller)),
);
},
)
],
);
}
@override
void dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
隐式动画
通过 Flutter 的 动画库,你可以为 UI 中的 widgets 添加动作并创造视觉效果。有些库包含各种各样可以帮你管理动画的 widget。这些 widgets 被统称为 隐式动画 或 隐式动画 widget ,其名字来源于它们所实现的 ImplicitlyAnimatedWidget
类。
使用隐式动画,可以通过设置一个目标值,驱动 widget 的属性进行动画变换;每当目标值发生变化时,属性会从旧值逐渐更新到新值。通过这种方式,隐式动画内部实现了动画控制,从而能够方便地使用隐式动画组件会管理动画效果,用户不需要再进行额外的处理。
推荐学习隐式动画教程
AnimatedOpacity
使用 AnimatedOpacity widget 对 opacity 属性进行动画。
AnimatedOpacity的构造方法如下:
vbnet
const AnimatedOpacity({
Key? key,
Widget? child,
required double opacity,
Curve curve = Curves.linear,
required Duration duration,
VoidCallback? onEnd,
boolean alwaysIncludeSemantics = false,
})
对应的参数:
- child:要控制透明度的子组件;
- opacity:最终的透明度值,取值范围从 0.0(不可见)到 1.0(完全可见);
- curve:动画曲线,默认是线性的Curves.linear,可以使用 Curves 来构建曲线效果;
- duration:动画时长;
- onEnd:动画结束回调方法;
- alwaysIncludeSemantics:是否总是包含语义信息,默认是 false。这个主要是用于辅助访问的,如果是 true,则不管透明度是多少,都会显示语义信息(可以辅助朗读),这对于视障人员来说会更友好。
scala
class FadeInDemo extends StatefulWidget {
const FadeInDemo({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _FadeInDemoState();
}
class _FadeInDemoState extends State<FadeInDemo> {
double opacity = 0.0;
@override
Widget build(BuildContext context) {
return Column(
children: [
const Padding(padding: EdgeInsets.only(top: 120)),
TextButton(
child: const Text(
'透明度变化',
style: TextStyle(color: Colors.blueAccent),
),
onPressed: () => setState(() {
if (opacity == 0.0) {
opacity = 1;
} else {
opacity = 0.0;
}
}),
),
AnimatedOpacity(
opacity: opacity,
duration: const Duration(seconds: 2),
child: const Icon(Icons.access_alarm, size: 200),
)
],
);
}
}
AnimatedContainer
使用 AnimatedContainer widget 让多个不同类型(double
和 Color
)的属性(margin
、borderRadius
和 color
)同时进行动画变换。
scss
double randomBorderRadius() {
return Random().nextDouble() * 64;
}
double randomMargin() {
return Random().nextDouble() * 64;
}
Color randomColor() {
return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF));
}
class AnimatedContainerDemo extends StatefulWidget {
const AnimatedContainerDemo({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _AnimatedContainerDemoState();
}
class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
late Color color;
late double borderRadius;
late double margin;
@override
void initState() {
super.initState();
color = randomColor();
borderRadius = randomBorderRadius();
margin = randomMargin();
}
void change() {
setState(() {
color = randomColor();
borderRadius = randomBorderRadius();
margin = randomMargin();
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
const Padding(padding: EdgeInsets.only(top: 120)),
TextButton(
child: const Text(
'change',
style: TextStyle(color: Colors.blueAccent),
),
onPressed: () => change()),
SizedBox(
width: 128,
height: 128,
child: AnimatedContainer(
margin: EdgeInsets.all(margin),
duration: const Duration(seconds: 2),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(borderRadius)),
curve: Curves.easeInOutBack,
),
)
],
);
}
}
Flutter API 中的 隐式动画:AnimatedAlign, AnimatedRotation, AnimatedScale, AnimatedPositioned, AnimatedSlide。
交织动画
交织动画是由一系列的小动画组成的动画。每个小动画可以是连续或间断的,也可以相互重叠。其关键点在于使用 Interval 给每个小动画设置一个时间间隔,以及为每个动画的设置一个取值范围 Tween,最后使用一个 AnimationController 控制总体的动画状态。
Interval 继承至 Curve 类,通过设置属性 begin 和 end 来确定这个小动画的运行范围。
scala
class Interval extends Curve {
/// 动画起始点
final double begin;
/// 动画结束点
final double end;
/// 动画缓动曲线
final Curve curve;
}
下面看一个例子,实现一个柱状图增长的动画:
- 开始时高度从0增长到300像素,同时颜色由绿色渐变为红色;这个过程占据整个动画时间的60%。
- 高度增长到300后,开始沿X轴向右平移100像素;这个过程占用整个动画时间的40%。
less
class StaggerAnimationWidget extends StatelessWidget {
StaggerAnimationWidget({Key? key, required this.controller})
: super(key: key) {
height = Tween(begin: 0.0, end: 300.0).animate(CurvedAnimation(
parent: controller,
curve: const Interval(
0.0, 0.6, //间隔,前60%的动画时间
curve: Curves.ease,
)));
padding = Tween(
begin: const EdgeInsets.only(left: 0.0),
end: const EdgeInsets.only(left: 100.0))
.animate(CurvedAnimation(
parent: controller,
curve: const Interval(0.6, 1.0, //间隔,后40%的动画时间
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,
),
),
);
}
late final Animation<double> controller;
late final Animation<double> height;
late final Animation<EdgeInsets> padding;
late final Animation<Color?> color;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (BuildContext context, child) {
return Container(
alignment: Alignment.bottomCenter,
padding: padding.value,
child: Container(
color: color.value,
width: 50,
height: height.value,
));
});
}
}
class StaggerAnimationDemo extends StatefulWidget {
const StaggerAnimationDemo({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _StaggerAnimationDemoState();
}
class _StaggerAnimationDemoState extends State<StaggerAnimationDemo>
with TickerProviderStateMixin {
late AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
}
void _playAnimation() async {
try {
//先正向执行动画
await controller.forward().orCancel;
//再反向执行动画
await controller.reverse().orCancel;
} on TickerCanceled {
//捕获异常。可能发生在组件销毁时,计时器会被取消。
}
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
const Padding(padding: EdgeInsets.only(top: 120)),
ElevatedButton(
onPressed: () => _playAnimation(),
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),
),
),
//调用我们定义的交错动画Widget
child: StaggerAnimationWidget(controller: controller),
),
],
),
);
}
}
Hero(跨页面共享元素)动画
Hero 指的是在页面(路由)间飞跃的 widget。简单来说 Hero 动画就是在路由切换时,有一个共享的widget 可以在新旧路由间切换。由于共享的 widget 在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会从旧路逐渐过渡到新路由中的指定位置,这样就会产生一个 Hero 动画。
你可能经常遇到 hero 动画。比如,页面上显示的代售商品列表。选择一件商品后,应用会跳转至包含更多细节以及"购买"按钮的新页面。在 Flutter 中,图像从当前页面转到另一个页面称为 hero 动画,相同的动作有时也被称为 共享元素过渡。
less
class HeroAnimationRouteA extends StatelessWidget {
const HeroAnimationRouteA({super.key});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.topCenter,
child: Column(
children: [
const Padding(padding: EdgeInsets.only(top: 120)),
InkWell(
child: Hero(
tag: "avatar",
child: ClipOval(
child: Image.asset(
"images/cat.jpeg",
width: 50,
),
),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const HeroAnimationRouteB()));
},
),
],
),
);
}
}
class HeroAnimationRouteB extends StatelessWidget {
const HeroAnimationRouteB({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Material(
child: InkWell(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: Image.asset("images/cat.jpeg"),
),
onTap: () {
Navigator.pop(context);
},
),
),
),
);
}
}
实现 Hero 动画只需要用Hero组件将要共享的 widget 包装起来,并提供一个相同的 tag 即可,中间的过渡帧都是 Flutter 框架自动完成的。必须要注意, 前后路由页的共享Hero的 tag 必须是相同的,Flutter 框架内部正是通过 tag 来确定新旧路由页widget的对应关系的。
页面转场动画
在不同路由(页面)之间进行切换的时候,许多设计语言,例如 Material 设计,都定义了一些标准行为。
Material组件库中提供了一个MaterialPageRoute组件,它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换。如果在Android上也想使用左右切换风格,一个简单的作法是可以直接使用CupertinoPageRoute。
但有时自定义路由动画会让 app 看上去更加的独特。为了更好的完成这一点,PageRouteBuilder提供了一个Animation对象,能够通过结合Tween以及Curve对象来自定义路由转换动画。
PageRouteBuilder中跟页面转场动画相关的参数只要有3个,
- pageBuilder:创建这个路由的内容
- transitionsBuilder:创建路由转换器,也就是路由动画
- transitionDuration:路由转换动画时长,默认是300毫秒
提示:transitionsBuilder 的 child 参数是通过 pageBuilder 方法来返回一个 transitionsBuilder widget,这个 pageBuilder 方法仅会在第一次构建路由的时候被调用。框架能够自动避免做额外的工作,因为整个过渡期间 child 保存了同一个实例。
下面看一个例子,使新页面从底部出来
scala
class Page1 extends StatelessWidget {
const Page1({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context).push(_createRoute());
},
child: const Text('Go!'),
);
}
}
Route _createRoute() {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => const Page2(),
transitionDuration: const Duration(seconds: 3),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
//页面从底部出来
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
const curve = Curves.ease;
//结合两个 tween,请使用 chain()
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween), //drive() 来创建一个新的 Animation<Offset>
child: child,
);
},
);
}
class Page2 extends StatelessWidget {
const Page2({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.black26,
child: const Center(
child: Text('Page 2'),
),
),
);
}
}