前言
提高用户体验和赋予应用程序更生动的外观,动画无疑是一项强大的工具。如何让你的应用脱颖而出?答案或许就隐藏在精心设计的动画中。通过巧妙运用动画功能,可以创造出令人惊叹的交互效果,让我们一起探索Flutter动画的魅力,为用户带来一场视觉盛宴吧...
在学习Flutter动画之前,先介绍一下flutter_anim_example,它是一个Flutter动画学习项目,涵盖了各种动画案例,让我们一起来深入探索!
动画基础
在学习动画之前,我们先来了解一下动画相关的基础类...
Animation
动画的核心类,它代表了在一段时间内生成值的抽象类。Animation类本身并不会直接产生动画效果,而是通过与其他类(如Tween、Curve、AnimationController等)结合来实现动画效果。下面介绍一下该类中一些常规方法:
| 方法名 | 返回值 | 描述 |
|---|---|---|
| addListener | void | 添加监听,每次动画值改变时调用监听器 |
| removeListener | void | 移除监听,每次动画值改变时停止调用监听器 |
| addStatusListener | void | 添加状态监听,每次动画状态改变时调用监听器AnimationStatus 动画状态是一个枚举类型,有以下四种状态forward:动画从头到尾运行completed:动画在结尾处停止reverse:动画从尾到头反向运行dismissed:动画在开始处停止 |
| removeStatusListener | void | 移除状态监听,每次动画状态改变时停止调用监听器 |
| drive | Animation | 创建一个新的动画,该动画的值根据给定的 Animatable 对象进行转换,将一个动画对象转换为另一种类型的动画,从而实现不同类型动画之间的转换和组合 |
AnimationController
AnimationController继承Animation类,并混入了AnimationEagerListenerMixin、AnimationLocalListenersMixin、AnimationLocalStatusListenersMixin类,用于控制动画的播放状态,包括开始、停止、反转等操作。它是管理动画的关键类之一,通常与Animation类结合使用来创建动画效果。
AnimationLocalListenersMixin:提供添加、移除和通知所有监听器的方法。
AnimationLocalStatusListenersMixin:提供添加、移除和通知所有监听器状态的方法。
在AnimationController内部有一个_tick(Duration elapsed)方法,用于处理动画的每一帧更新,更新动画状态、通知监听器、处理动画完成等操作。
下面介绍一下该类中一些常规方法:
| 方法名 | 描述 |
|---|---|
| resync | 使用新的 [TickerProvider] 重新创建 [Ticker],并重新与新的TickerProvider进行同步 |
| value | 这是一个getter方法,表示动画当前的值 |
| reset | 将动画重置为起点或关闭状态 |
| forward | 开始从头到尾运行动画。 |
| reverse | 开始从尾到头运行动画 |
| stop | 立即停止动画 |
| fling | 启动一个惯性动画来停止动画 |
| animateTo | 从当前位置到指定位置运行动画(正向) |
| animateBack | 从当前位置到指定位置运行动画(反向) |
| repeat | 开始从头到尾运行动画,并在动画完成时重新启动动画。将 [reverse] 设置为 true后将不再是从头到尾运行,起始值将在每次重复时在 [min] 和 [max] 值之间交替 |
| animateWith | 使用物理模拟(Simulation)来驱动动画 |
| dispose | 释放资源,调用此方法后,该对象将不再可用 |
Ticker
Ticker是用于管理动画帧更新时机的类,它依赖于Flutter框架中提供的调度器(SchedulerBinding)来触发动画帧的更新,确保动画在每一帧更新时都能得到正确的处理。
TickerProvider
TickerProvider是一个抽象类,用于提供一个Ticker对象。在创建一个AnimationController对象时,我们必须要传入一个TickerProvider对象。
TickerProviderStateMixin
TickerProviderStateMixin是一个mixin类,用于在State对象中创建和管理多个Ticker对象,适用于需要管理多个动画控制器的情况,通常情况下不需要。
SingleTickerProviderStateMixin
SingleTickerProviderStateMixin是一个mixin类,用于在State对象中创建和管理单个Ticker对象,适用于只需要管理单个动画控制器的情况。
ImplicitlyAnimatedWidget
ImplicitlyAnimatedWidget是一个抽象类,用于创建包含隐式动画的自定义动画组件。它简化了创建动画效果的过程,开发人员只需关注动画的属性变化,而不需要手动处理动画的控制逻辑。
ImplicitlyAnimatedWidgetState
ImplicitlyAnimatedWidgetState是ImplicitlyAnimatedWidget的状态类,负责管理动画的状态和动画控制器。
Animatable
Animatable是一个抽象类,用于定义动画的插值方式。它提供了一个evaluate方法,该方法接受一个double类型的参数(通常是0.0到1.0之间的值),并返回相应的动画值。Animatable通常用作Tween类的基类,用于定义动画的取值范围和插值方式。
Tween
Tween类继承了Animatable类,在Flutter中用于定义补间动画的起始值、结束值,以便确定动画变化范围,插值器根据动画的时间值(通常是0.0到1.0之间)来计算出对应的动画值,从而实现动画值的平滑过渡。
Curve
Curve类是一个抽象类,常用于定义动画的时间变化曲线。Flutter提供了许多内置的曲线,请参考: Curves
隐式动画
可直接使用在线 Dart 编辑器运行看效果
淡入淡出(AnimatedOpacity)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
double opacity = 1.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
opacity = opacity == 1.0 ? 0.1 : 1.0;
});
},
child: AnimatedOpacity(
opacity: opacity,
curve: Curves.decelerate,
duration: const Duration(seconds: 1),
child: const Column(
children: [
Text('Age: 28'),
Text('Sex: Male'),
Text('Name: Zhang san'),
Divider(
height: 16,
thickness: 1,
color: Colors.grey,
),
],
),
onEnd: () {
print('Animation completed');
},
),
),
);
}
}
形状变化(AnimatedContainer)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
Decoration decoration = BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
if (change) {
decoration = BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(100),
);
} else {
decoration = BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
);
}
});
},
child: Center(
child: AnimatedContainer(
width: 200,
height: 200,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
decoration: decoration,
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 300),
onEnd: () {
print('Animation completed');
},
child: const Icon(Icons.add, color: Colors.white, size: 68),
),
),
),
);
}
}
文本变化(AnimatedDefaultTextStyle)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
TextStyle textStyle = const TextStyle(
color: Colors.blue,
fontSize: 16,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
if (change) {
textStyle = const TextStyle(
color: Colors.red,
fontSize: 48,
fontWeight: FontWeight.bold,
);
} else {
textStyle = const TextStyle(
color: Colors.blue,
fontSize: 16,
);
}
});
},
child: Center(
child: AnimatedDefaultTextStyle(
curve: Curves.decelerate,
duration: const Duration(milliseconds: 300),
style: textStyle,
child: const Text(
"Hello World!",
),
onEnd: () {
print("Animation completed");
},
),
),
),
);
}
}
交叉变化(AnimatedCrossFade)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Center(
child: AnimatedCrossFade(
firstCurve: Curves.fastOutSlowIn,
crossFadeState:
change ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: const Duration(milliseconds: 300),
firstChild: const Icon(
Icons.radio_button_unchecked,
size: 68,
color: Colors.blue,
),
secondChild: const Icon(
Icons.check_circle,
size: 68,
color: Colors.blue,
),
alignment: Alignment.centerLeft,
sizeCurve: Curves.easeInOut,
),
),
),
);
}
}
切换变化(AnimatedSwitcher)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Center(
child: AnimatedSwitcher(
switchInCurve: Curves.easeIn,
switchOutCurve: Curves.easeInOut,
duration: const Duration(seconds: 1),
child: change
? const Icon(
Icons.radio_button_unchecked,
size: 68,
color: Colors.blue,
)
: const Icon(
Icons.check_circle,
size: 68,
color: Colors.blue,
),
),
),
),
);
}
}
填充变化(AnimatedPadding)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Center(
child: AnimatedPadding(
padding: EdgeInsets.all(change ? 100 : 0.0),
curve: Curves.linear,
duration: const Duration(milliseconds: 300),
child: Container(
color: Colors.blue,
),
onEnd: () {
print('Animation completed');
},
),
),
),
);
}
}
定位变化(AnimatedPositioned)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Stack(
children: [
AnimatedPositioned(
width: 200,
height: 200,
top: change ? 0 : 500,
left: change ? 0 : 300,
curve: Curves.linear,
duration: const Duration(milliseconds: 300),
child: Container(
color: Colors.blue,
),
onEnd: () {
print('Animation Completed');
},
)
],
),
),
);
}
}
定向变化(AnimatedPositionedDirectional)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Stack(
children: [
AnimatedPositionedDirectional(
width: 200,
height: 200,
start: change ? 0 : 300,
top: change ? 0 : 500,
curve: Curves.linear,
duration: const Duration(milliseconds: 300),
child: Container(
color: Colors.blue,
),
onEnd: () {
print('Animation Completed');
},
)
],
),
),
);
}
}
对齐变化(AnimatedAlign)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Stack(
children: [
AnimatedAlign(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
alignment: change ? Alignment.topCenter : Alignment.bottomCenter,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
onEnd: () {
print('Animation completed');
},
)
],
),
),
);
}
}
缩放变化(AnimatedScale)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Center(
child: AnimatedScale(
scale: change ? 2.0 : 1.0,
alignment: Alignment.center,
curve: Curves.linear,
duration: const Duration(milliseconds: 300),
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
onEnd: () {
print('Animation completed');
},
),
),
),
);
}
}
旋转变化(AnimatedRotation)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Center(
child: AnimatedRotation(
turns: change ? 1 : 0,
alignment: Alignment.center,
curve: Curves.easeInCubic,
duration: const Duration(milliseconds: 300),
child: const Icon(
Icons.refresh,
size: 100,
color: Colors.blue,
),
onEnd: () {
print('Animation completed');
},
),
),
),
);
}
}
滑动变化(AnimatedSlide)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Center(
child: AnimatedSlide(
offset: change ? const Offset(1, 0) : const Offset(0, 0),
curve: Curves.easeOutSine,
duration: const Duration(milliseconds: 200),
child: const Icon(
Icons.directions_run,
size: 100,
color: Colors.blue,
),
onEnd: () {
print('Animation completed');
},
),
),
),
);
}
}
透明变化(SliverAnimatedOpacity)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Center(
child: CustomScrollView(
slivers: [
SliverAnimatedOpacity(
opacity: change ? 0.2 : 1.0,
curve: Curves.easeInOutQuint,
duration: const Duration(milliseconds: 300),
sliver: SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
height: 50,
color: Colors.blue,
alignment: Alignment.center,
child: Text(
'Item $index',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
),
);
},
childCount: 20,
),
itemExtent: 50,
),
onEnd: () {
print('Animation completed');
},
),
],
),
),
),
);
}
}
物理变化(AnimatedPhysicalModel)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Center(
child: AnimatedPhysicalModel(
curve: Curves.linear,
shape: BoxShape.rectangle,
elevation: change ? 0 : 16,
duration: const Duration(milliseconds: 500),
color: change ? Colors.yellow : Colors.blue,
shadowColor: change ? Colors.yellow : Colors.blue,
borderRadius: BorderRadius.circular(change ? 0 : 16),
child: Container(
width: 200,
height: 200,
alignment: Alignment.center,
child: const Text(
'Hello World!',
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
),
onEnd: () {
print('Animation Completed');
},
),
),
),
);
}
}
展开变化(AnimatedFractionallySizedBox)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Center(
child: AnimatedFractionallySizedBox(
duration: const Duration(seconds: 1),
widthFactor: change ? 1.0 : 0.5,
heightFactor: change ? 1.0 : 0.5,
alignment: Alignment.center,
child: Container(
color: Colors.blue,
alignment: Alignment.center,
child: Text(
change ? '收缩' : '展开',
style: const TextStyle(color: Colors.white, fontSize: 20),
),
),
onEnd: () {},
),
),
),
);
}
}
补间变化(TweenAnimationBuilder)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
bool change = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: GestureDetector(
onTap: () {
setState(() {
change = !change;
});
},
child: Center(
child: TweenAnimationBuilder<Color?>(
tween: ColorTween(end: change ? Colors.blue : Colors.yellow),
duration: const Duration(seconds: 1),
builder: (context, color, child) {
return TweenAnimationBuilder<double>(
tween: Tween<double>(end: change ? 200 : 100),
duration: const Duration(seconds: 1),
builder: (context, size, child) {
return Container(
width: size,
height: size,
color: color,
alignment: Alignment.center,
child: const Text(
'Hello World!',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 20,
),
),
);
},
);
},
),
),
),
);
}
}
小结
隐式动画通常都以Animated+开头,并继承了ImplicitlyAnimatedWidget类,开发者无需手动管理AnimationController的生命周期,通过改变动画属性值,自动产生动画效果,简化了动画的实现过程。但也有一定局限性,除了改变动画属性值之外,开发者只能为动画选择“持续时间”和“曲线”。
显示动画
SizeTransition
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with TickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _sizeFactor;
late final AnimationController _controller2;
late final Animation<double> _sizeFactor2;
bool isVertical = true;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_sizeFactor = CurvedAnimation(
parent: _controller,
curve: Curves.decelerate,
);
_controller.addStatusListener((status) {
// 动画开始、结束、正向或反向时回调
switch (status) {
case AnimationStatus.forward:
// 动画开始
break;
case AnimationStatus.completed:
// 动画完成
break;
case AnimationStatus.reverse:
// 动画开始(倒放)
break;
case AnimationStatus.dismissed:
// 动画完成(回到起始位置)
setState(() {
isVertical = !isVertical;
});
break;
}
});
// 通常情况下,只需要一个动画控制器,这里只是为了示范TickerProviderStateMixin和SingleTickerProviderStateMixin的区别
_controller2 = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_sizeFactor2 = CurvedAnimation(
parent: _controller2,
curve: Curves.decelerate,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
Expanded(
child: animWidget(),
),
const Divider(
indent: 16,
endIndent: 16,
height: 32,
thickness: 0.5,
color: Colors.grey,
),
ElevatedButton(
onPressed: startAnimation,
child: const Padding(
padding: EdgeInsets.all(12), child: Text('Start Animation')),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget animWidget() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizeTransition(
sizeFactor: _sizeFactor,
axis: isVertical ? Axis.vertical : Axis.horizontal,
axisAlignment: -1,
fixedCrossAxisSizeFactor: 1,
child: Container(
width: 128,
height: 128,
color: Colors.blue,
margin: const EdgeInsets.symmetric(horizontal: 20),
),
),
const SizedBox(height: 16),
SizeTransition(
sizeFactor: _sizeFactor2,
axis: isVertical ? Axis.vertical : Axis.horizontal,
axisAlignment: -1,
fixedCrossAxisSizeFactor: 1,
child: Container(
width: 128,
height: 128,
color: Colors.orange,
margin: const EdgeInsets.symmetric(horizontal: 20),
),
),
],
);
}
startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
if (_controller2.isCompleted) {
_controller2.reverse();
} else {
_controller2.forward();
}
}
@override
void dispose() {
_controller.dispose();
_controller2.dispose();
super.dispose();
}
}
SlideTransition
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<Offset> position;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
position = Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.5, 0),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
Expanded(
child: animWidget(),
),
const Divider(
indent: 16,
endIndent: 16,
height: 32,
thickness: 0.5,
color: Colors.grey,
),
ElevatedButton(
onPressed: startAnimation,
child: const Padding(
padding: EdgeInsets.all(12), child: Text('Start Animation')),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget animWidget() {
return Center(
child: SlideTransition(
position: position,
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.blue,
),
),
);
}
startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
ScaleTransition
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _scale;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scale = CurvedAnimation(
parent: _controller,
curve: Curves.fastOutSlowIn,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
Expanded(
child: animWidget(),
),
const Divider(
indent: 16,
endIndent: 16,
height: 32,
thickness: 0.5,
color: Colors.grey,
),
ElevatedButton(
onPressed: startAnimation,
child: const Padding(
padding: EdgeInsets.all(12), child: Text('Start Animation')),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget animWidget() {
return Center(
child: ScaleTransition(
scale: _scale,
child: Container(
width: 128,
height: 128,
color: Colors.blue,
),
),
);
}
startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
RotationTransition
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> turns;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
turns = Tween<double>(
begin: 0,
end: 1,
).animate(_controller);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
Expanded(
child: animWidget(),
),
const Divider(
indent: 16,
endIndent: 16,
height: 32,
thickness: 0.5,
color: Colors.grey,
),
ElevatedButton(
onPressed: startAnimation,
child: const Padding(
padding: EdgeInsets.all(12), child: Text('Start Animation')),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget animWidget() {
return Center(
child: RotationTransition(
turns: turns,
child: Container(
width: 128,
height: 128,
color: Colors.blue,
),
),
);
}
startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
FadeTransition
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _opacity;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_opacity = CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
Expanded(
child: animWidget(),
),
const Divider(
indent: 16,
endIndent: 16,
height: 32,
thickness: 0.5,
color: Colors.grey,
),
ElevatedButton(
onPressed: startAnimation,
child: const Padding(
padding: EdgeInsets.all(12), child: Text('Start Animation')),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget animWidget() {
return Center(
child: FadeTransition(
opacity: _opacity,
child: Container(
width: 128,
height: 128,
color: Colors.blue,
margin: const EdgeInsets.symmetric(horizontal: 20),
),
),
);
}
startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
PositionedTransition
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
Expanded(
child: animWidget(),
),
const Divider(
indent: 16,
endIndent: 16,
height: 32,
thickness: 0.5,
color: Colors.grey,
),
ElevatedButton(
onPressed: startAnimation,
child: const Padding(
padding: EdgeInsets.all(12), child: Text('Start Animation')),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget animWidget() {
// PositionedTransition是Stack的一个子组件,它使用绝对定位来控制子组件的位置
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
const double iconSize = 100;
final Size biggest = constraints.biggest;
Rect begin = Rect.fromLTWH((biggest.width - iconSize) / 2,
biggest.height - iconSize, iconSize, iconSize);
Rect end = Rect.fromLTWH(
(biggest.width - iconSize) / 2, 0, iconSize, iconSize);
return Stack(
children: <Widget>[
PositionedTransition(
rect: RelativeRectTween(
begin: RelativeRect.fromSize(
begin,
biggest,
),
end: RelativeRect.fromSize(
end,
biggest,
),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.elasticInOut,
)),
child: const Icon(
Icons.airplanemode_on,
size: iconSize,
color: Colors.blue,
),
),
],
);
},
);
}
startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
RelativePositionedTransition
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
Expanded(
child: animWidget(),
),
const Divider(
indent: 16,
endIndent: 16,
height: 32,
thickness: 0.5,
color: Colors.grey,
),
ElevatedButton(
onPressed: startAnimation,
child: const Padding(
padding: EdgeInsets.all(12), child: Text('Start Animation')),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget animWidget() {
// RelativePositionedTransition是Stack的一个子组件,它使用相对定位来控制子组件的位置
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
const double iconSize = 100;
final Size biggest = constraints.biggest;
Rect begin = Rect.fromLTWH((biggest.width - iconSize) / 2,
biggest.height - iconSize, iconSize, iconSize);
Rect end = Rect.fromLTWH(
(biggest.width - iconSize) / 2, 0, iconSize, iconSize);
return Stack(
children: <Widget>[
RelativePositionedTransition(
size: biggest,
rect: RectTween(
begin: begin,
end: end,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.elasticInOut,
)),
child: const Icon(
Icons.airplanemode_on,
size: iconSize,
color: Colors.blue,
),
),
],
);
},
);
}
startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
DefaultTextStyleTransition
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<TextStyle> _style;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_style = TextStyleTween(
begin: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.normal,
),
end: const TextStyle(
color: Colors.blue,
fontSize: 32,
fontWeight: FontWeight.bold,
),
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.decelerate,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
Expanded(
child: animWidget(),
),
const Divider(
indent: 16,
endIndent: 16,
height: 32,
thickness: 0.5,
color: Colors.grey,
),
ElevatedButton(
onPressed: startAnimation,
child: const Padding(
padding: EdgeInsets.all(12), child: Text('Start Animation')),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget animWidget() {
return Center(
child: DefaultTextStyleTransition(
style: _style,
textAlign: TextAlign.center,
child: const Text('Hello World!'),
),
);
}
startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
DecoratedBoxTransition
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<Decoration> _decoration;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
final DecorationTween decorationTween = DecorationTween(
begin: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(64.0),
boxShadow: const <BoxShadow>[
BoxShadow(
color: Colors.blue,
blurRadius: 8.0,
spreadRadius: 2.0,
offset: Offset(0, 4.0),
),
],
),
end: BoxDecoration(
color: Colors.white,
boxShadow: const <BoxShadow>[
BoxShadow(
color: Colors.blue,
blurRadius: 8.0,
spreadRadius: 2.0,
offset: Offset(0, 4.0),
),
],
borderRadius: BorderRadius.circular(8.0),
// No shadow.
),
);
_decoration = decorationTween.animate(
CurvedAnimation(
parent: _controller,
curve: Curves.ease,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
Expanded(
child: animWidget(),
),
const Divider(
indent: 16,
endIndent: 16,
height: 32,
thickness: 0.5,
color: Colors.grey,
),
ElevatedButton(
onPressed: startAnimation,
child: const Padding(
padding: EdgeInsets.all(12), child: Text('Start Animation')),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget animWidget() {
return Center(
child: DecoratedBoxTransition(
decoration: _decoration,
position: DecorationPosition.background,
child: const Icon(
Icons.add_circle,
size: 128,
color: Colors.blue,
),
),
);
}
startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
AlignTransition
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<AlignmentGeometry> _alignment;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
_alignment = Tween<Alignment>(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.ease,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
Expanded(
child: animWidget(),
),
const Divider(
indent: 16,
endIndent: 16,
height: 32,
thickness: 0.5,
color: Colors.grey,
),
ElevatedButton(
onPressed: startAnimation,
child: const Padding(
padding: EdgeInsets.all(12), child: Text('Start Animation')),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget animWidget() {
return AlignTransition(
alignment: _alignment,
child: Container(
width: 240,
height: 48,
color: Colors.blue,
margin: const EdgeInsets.symmetric(horizontal: 20),
),
);
}
startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
小结
显式动画是一种更灵活、更具定制性的动画实现方式,通过使用Animation和AnimationController等类来手动控制动画的每个阶段,可以实现各种复杂的动画效果。
补间动画
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with TickerProviderStateMixin {
late List<AnimationController> controllers;
late List<Animation<double>> animations;
bool isFlipX = false;
bool isFlipY = false;
@override
void initState() {
super.initState();
controllers = List.generate(4, (index) {
return AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
});
animations = controllers.mapIndexed((index, controller) {
switch (index) {
case 0:
return Tween<double>(begin: 0.0, end: 2 * 3.14).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
),
);
case 1:
return Tween<double>(begin: 0, end: 100).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
),
);
case 2:
return Tween<double>(begin: 1.0, end: 2.0).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
),
)..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
}
});
case 3:
return Tween<double>(begin: 100, end: 200).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
),
)..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
}
});
}
return Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
),
);
}).toList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('补间动画'),
),
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ListView(
children: <Widget>[
_buildDescriptionText(),
_buildDivider(),
_buildAnimatedWidget(0),
_buildDivider(),
_buildAnimatedWidget(1),
_buildDivider(),
_buildAnimatedWidget(2),
_buildDivider(),
_buildAnimatedWidget(3),
_buildDivider(),
_buildFlipWidget(),
],
),
),
);
}
Widget _buildDescriptionText() {
return const Text('补间动画在两个关键帧之间创建平滑的过渡动画,下面使用Animation和Tween类来实现补间动画');
}
Widget _buildDivider() {
return const Divider(
height: 32,
thickness: 1,
color: Colors.grey,
);
}
Widget _buildAnimatedWidget(int index) {
return GestureDetector(
onTap: () {
startAnimation(index);
},
child: AnimatedBuilder(
animation: animations[index],
builder: (context, child) {
if (index == 0) {
return _buildRotateWidget(
animations[index], Icons.refresh, Colors.blue);
} else if (index == 1) {
return _buildTranslateWidget(
animations[index], Icons.accessibility, Colors.orange);
} else if (index == 2) {
return _buildScaleWidget(
animations[index], Icons.favorite, Colors.red);
} else if (index == 3) {
return _buildIconWidget(
animations[index], Icons.star, Colors.yellow);
}
return Container();
},
),
);
}
Widget _buildRotateWidget(
Animation<double> animation, IconData icon, Color color) {
return Transform.rotate(
angle: animation.value,
child: Icon(
icon,
color: color,
size: 80,
),
);
}
Widget _buildTranslateWidget(
Animation<double> animation, IconData icon, Color color) {
return Transform.translate(
offset: Offset(animation.value, 0),
child: Icon(
icon,
color: color,
size: 80,
),
);
}
Widget _buildScaleWidget(
Animation<double> animation, IconData icon, Color color) {
return Transform.scale(
scale: animation.value,
child: Icon(
icon,
color: color,
size: 80,
),
);
}
Widget _buildIconWidget(
Animation<double> animation, IconData icon, Color color) {
return GestureDetector(
onTap: () {
startAnimation(3);
},
child: Icon(
icon,
color: color,
size: animation.value,
),
);
}
Widget _buildFlipWidget() {
return GestureDetector(
onTap: () {
setState(() {
flip();
});
},
child: Transform.flip(
flipX: isFlipX,
flipY: isFlipY,
child: const Icon(
Icons.flip,
color: Colors.green,
size: 80,
),
),
);
}
void startAnimation(int index) {
if (controllers.length > index && index >= 0) {
if (controllers[index].isDismissed) {
controllers[index].forward();
} else {
controllers[index].reverse();
}
}
}
void flip() {
isFlipX = !isFlipX;
isFlipY = !isFlipY;
}
@override
void dispose() {
for (var controller in controllers) {
controller.dispose();
}
super.dispose();
}
}
extension IterableIndexed<E> on Iterable<E> {
Iterable<T> mapIndexed<T>(T Function(int index, E e) f) sync* {
var index = 0;
for (var element in this) {
yield f(index, element);
index++;
}
}
}
小结
补间动画属于显示动画的范畴,它通过Tween类来定义动画的起始值和结束值,从而确定动画的变化范围,开发人员可以通过补间动画实现各种复杂的动画效果,如缩放、平移、旋转等。
Hero动画
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
padding: const EdgeInsets.all(32),
alignment: Alignment.topLeft,
child: HeroWidget(
tag: 'hero',
width: 100,
onTap: () {
startPageHero();
},
),
),
);
}
startPageHero() async {
Widget child = Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.grey[200],
alignment: Alignment.center,
child: HeroWidget(
tag: 'hero',
width: 300,
onTap: () {
Navigator.pop(context);
},
),
),
);
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return child;
},
),
);
}
}
class HeroWidget extends StatelessWidget {
final double width;
final String tag;
final VoidCallback? onTap;
const HeroWidget({
super.key,
required this.width,
required this.tag,
this.onTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Hero(
tag: tag,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Icon(Icons.person, size: width),
),
),
),
);
}
}
小结
Hero动画是一种让应用在切换页面时更加顺畅的特效。它通过将两个页面中相同的元素用Hero组件包裹起来,并用相同的标签来标识它们,实现了元素之间的平滑过渡动画效果。
交织动画
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late List<Animation<double>> animations;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
animations = List.generate(10, (index) {
return Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
index * 0.1,
(index + 1) * 0.1,
curve: Curves.easeIn,
),
),
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: <Widget>[
Expanded(child: animWidget()),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
startAnimation();
},
child: const Text('Start Animation'),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget animWidget() {
return ListView(
children: List.generate(
10,
(index) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: animations[index].value,
child: Container(
width: double.infinity,
margin:
const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
height: 48,
color: Colors.blue[200],
alignment: Alignment.center,
child: Text(
'Item $index',
style: const TextStyle(
fontSize: 18,
color: Colors.black,
),
),
),
);
},
);
},
),
);
}
startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
小结
在交织动画中,每个元素都可以有自己的动画开始时间、持续时间、延迟等属性,使得它们可以以错开的时间序列展示动画效果。这种错开的动画效果可以让用户的视觉焦点更加丰富,增加页面的活力和吸引力。
列表动画
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
int? selectIndex;
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
List<String> listData = List.generate(3, (index) => 'Item $index');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('列表动画'),
),
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Stack(
children: [
AnimatedList(
key: listKey,
initialItemCount: listData.length,
itemBuilder: (context, index, animation) {
return buildItem(context, index, animation);
},
),
Positioned(
bottom: 20,
right: 20,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
insert();
},
child: const Text('+'),
),
ElevatedButton(
onPressed: () {
remove();
},
child: const Text('-'),
),
],
),
),
],
),
),
);
}
Widget buildItem(
BuildContext context, int index, Animation<double> animation) {
return SizeTransition(
sizeFactor: animation,
child: Card(
color: selectIndex == index ? Colors.blue[300] : null,
child: ListTile(
title: Text(listData[index]),
onTap: () {
setState(() {
selectIndex = index;
});
},
),
),
);
}
insert() {
final int index = selectIndex != null ? selectIndex! : listData.length;
listData.insert(index, 'Item ${listData.length}');
listKey.currentState!.insertItem(index);
}
remove() {
final int index = selectIndex != null ? selectIndex! : listData.length - 1;
if (index < 0) return;
final String text = listData[index];
listKey.currentState!.removeItem(
index,
(context, animation) => SizeTransition(
sizeFactor: animation,
child: Card(
color: selectIndex == index ? Colors.blue[300] : null,
child: ListTile(
title: Text(text),
),
),
),
);
listData.removeAt(index);
selectIndex = null;
}
}
小结
列表动画在列表执行添加、删除等操作时实现平滑的动画效果,可以增强用户体验和提升应用的视觉吸引力。
转场动画
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('页面转场动画'), centerTitle: true),
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
startPagePanUpDown();
},
child: const Text('上下平移'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
startPagePanLeftRight();
},
child: const Text('左右平移'),
),
],
),
),
);
}
void startPagePanUpDown() async {
PageRouteBuilder routeBuilder = PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const OtherView(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
final tween = Tween(begin: begin, end: end);
final curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.ease,
);
return SlideTransition(
position: tween.animate(curvedAnimation),
child: child,
);
},
);
Navigator.of(context).push(routeBuilder);
}
void startPagePanLeftRight() async {
PageRouteBuilder routeBuilder = PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const OtherView(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
final tween = Tween(begin: begin, end: end);
final curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.ease,
);
return SlideTransition(
position: tween.animate(curvedAnimation),
child: child,
);
},
);
Navigator.of(context).push(routeBuilder);
}
}
class OtherView extends StatelessWidget {
const OtherView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('其他页面')),
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
alignment: Alignment.center,
child: const Text(
'Hello OtherView!',
style: TextStyle(fontSize: 24, color: Colors.black),
),
),
);
}
}
小结
转场动画是一种用于在页面之间实现平滑过渡效果的动画,可以增强用户体验和提升应用的视觉吸引力。
物理动画
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
/// 动画控制器
late final AnimationController _controller;
/// 定义一个新的对齐动画
late Animation<Alignment> _animation;
/// left: -1, right: 1, top: -1, bottom: 1, center: 0
var _dragAlignment = Alignment.center;
/// 是否垂直方向
bool isVertical = true;
/// 动画是否正在运行
bool running = false;
@override
void initState() {
super.initState();
_controller = AnimationController.unbounded(vsync: this)
..addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// 动画完成
setState(() {
isVertical = !isVertical;
running = false;
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('弹簧')),
body: Container(
width: double.infinity,
height: double.infinity,
color: Colors.white,
child: Column(
children: [
Expanded(
child: Align(
alignment: _dragAlignment,
child: SizedBox(
width: 120,
height: 120,
child: Stack(
alignment: Alignment.center,
children: [
for (int i = 0; i < 12; i++)
Transform.rotate(
angle: i * 30 * 0.0174533,
child: Align(
alignment: Alignment.topCenter,
child: Container(
width: 80,
height: 10,
color: Colors.red,
),
),
),
Container(
width: 110,
height: 110,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.orange,
),
),
],
),
),
)),
const SizedBox(height: 48),
ElevatedButton(
onPressed: () {
startAnimation();
},
child: const Text('Start Animation'),
),
const SizedBox(height: 24)
],
),
),
);
}
/// 开始动画
void startAnimation() {
if (running) {
return;
}
running = true;
// 用于创建一个从begin到end的动画
if (isVertical) {
_animation = _controller.drive(
AlignmentTween(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
);
} else {
_animation = _controller.drive(
AlignmentTween(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
);
}
// 定义一个弹簧的物理特性,mass:质量,stiffness:刚度,damping:阻尼
const spring = SpringDescription(
mass: 10, // 值越大动画的质量越稳定,但是速度越慢,其值通常为正数,但没有严格的上限
stiffness: 800, // 其值通常为正数,刚度越大弹性越强
damping: 0.2 // 其值通常为正数,阻尼越大震荡越快平息
);
// SpringSimulation: 基于弹簧物理模拟的动画的类
// spring:弹簧的物理特性,start:动画开始位置,end:动画结束位置,velocity:动画的初始速度
final simulation = SpringSimulation(spring, 0, 0.5, 0);
_controller.animateWith(simulation);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
小结
物理动画是基于物理学原理的一类动画形式,利用物理学的模型来创建动态、自然的效果,而不是基于预定义的关键帧。物理动画通过模拟真实世界中的物理特性,让用户界面更加生动、自然、符合直觉。
预置动画
预置动画是指Flutter官方开源的 animations package,这个 package 包含了以下内置常用模式: Container 变换、共享轴变化、渐变穿透和渐变变换。
总结
Flutter提供了多种动画类型:隐式动画自动处理动画过渡,显示动画直接显示变化,补间动画控制动画过程,Hero动画在页面间传递元素,交织动画同时播放多个动画,列表动画为列表项添加动画效果,转场动画页面切换时动画效果,物理动画模拟真实物理效果,预置动画提供预定义动画效果。