Flutter 之动画
1、原理
1、动画形成
在任何系统的 UI 框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观,由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画,这和电影的原理是一样的。我们将 UI 的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率 FPS(Frame Per Second)即每秒的动画帧数。很明显,帧率越高则动画就会越流畅!一般情况下,对于人眼来说,动画帧率超过 16 FPS,就基本能看了,超过 32 FPS 就会感觉相对平滑,而超过 32FPS,大多数人基本上就感受不到差别了。由于动画的每一帧都是要改变 UI 输出,所以在一个时间段内连续的改变 UI 输出是比较耗资源的,对设备的软硬件系统要求都较高,所以在 UI 系统中,动画的平均帧率是重要的性能指标,而在 Flutter 中,理想情况下是可以实现 60FPS 的,这和原生应用能达到的帧率是基本是持平的。
2、Flutter 动画分类
FLutter 中的动画主要分为:隐式动画、显式动画、自定义隐式动画、自定义显式动画和 Hero 动画。
2、隐式动画
通过几行代码就可以实现隐式动画,由于隐式动画背后的实现原理和繁琐的操作细节都被隐去了,所以叫隐式动画。FLutter 中提供的 AnimatedContainer、AnimatedPadding、AnimatedPositioned、AnimatedOpacity、AnimatedDefaultTextStyle、AnimatedSwitcher 都属于隐式动画。隐式动画中可以通过 duration 配置动画时长、可以通过 Curve (曲线)来配置动画过程。
1、AnimatedContainer
AnimatedContainer 的属性和 Container 属性基本是一样的,当 AnimatedContainer 属性改变的时候就会触发动画。
属性
(new) AnimatedContainer AnimatedContainer({
Key? key,
AlignmentGeometry? alignment, //子元素相对于容器的对齐方式
EdgeInsetsGeometry? padding, //子元素的内边距
Color? color, //容器背景颜色(decoration 也能设置背景色,两个不要同时使用)
Decoration? decoration, //容器的边框修饰
Decoration? foregroundDecoration, //容器的前景边框修饰(使用时会挡住 color 或 decoration 的颜色)
double? width, //容器的宽
double? height, //容器的高
BoxConstraints? constraints, //容器的大小约束,可以指定最小宽高、最大宽高,width 和 height 即使设置更大的宽高也不会有效果.
EdgeInsetsGeometry? margin, //容器的外边距
Matrix4? transform, //容器的 Matrix 变换
AlignmentGeometry? transformAlignment, //transform 不为空时有效,转换的对齐方式,可以理解为起点位置
Widget? child,
Clip clipBehavior = Clip.none, //在decoration不为空的情况下,才有效果,指定剪切的模式,
Curve curve = Curves.linear, //动画运用的曲线
required Duration duration, //动画时长
void Function()? onEnd, //动画执行结束的回调
})
使用
class AnimatedContainerPage extends StatefulWidget {
const AnimatedContainerPage({super.key});
@override
State<AnimatedContainerPage> createState() => _AnimatedContainerPageState();
}
class _AnimatedContainerPageState extends State<AnimatedContainerPage> {
bool flag = false;
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
flag = !flag;
});
},
child: const Icon(Icons.animation),
),
appBar: AppBar(
title: const Text('AnimatedContainer'),
),
body: Center(
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: flag ? 100 : 300,
height: flag ? 100 : 300,
color: Colors.green,
),
),
);
}
}
2、AnimatedPadding
属性
(new) AnimatedPadding AnimatedPadding({
Key? key,
required EdgeInsetsGeometry padding, //子元素的内边距
Widget? child,
Curve curve = Curves.linear, //动画运用的曲线
required Duration duration, //动画时长
void Function()? onEnd, //动画执行结束的回调
})
使用
class AnimatedPaddingPage extends StatefulWidget {
const AnimatedPaddingPage({super.key});
@override
State<AnimatedPaddingPage> createState() => _AnimatedPaddingPageState();
}
class _AnimatedPaddingPageState extends State<AnimatedPaddingPage> {
bool flag = false;
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.change_circle),
onPressed: () {
setState(() {
flag = !flag;
});
}),
appBar: AppBar(
title: const Text('AnimatedPadding'),
),
body: AnimatedPadding(
duration: const Duration(milliseconds: 3000),
padding: EdgeInsets.fromLTRB(10, flag ? 10 : 500, 0, 0),
curve: Curves.bounceInOut,
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
);
}
}
3、AnimatedPositioned
AnimatedPositioned 是 Stack 组件中的 Positioned 的动画替换组件。可以通过 AnimatedPositioned 实现组件在 Stack 组件的位置,从而实现相对 Stack 组件的移动效果。需要注意的是横向参数(left、right 和 width)、纵向参数(top、bottom 和 height)只能从3个里面选2个设置,否则会导致布局冲突。
属性
AnimatedPositioned AnimatedPositioned({
Key? key,
required Widget child,
double? left,
double? top,
double? right,
double? bottom,
double? width,
double? height,
Curve curve = Curves.linear, //动画运用的曲线
required Duration duration, //动画时长
void Function()? onEnd, //动画结束后回调
})
使用
class AnimatedPositionedPage extends StatefulWidget {
const AnimatedPositionedPage({super.key});
@override
State<AnimatedPositionedPage> createState() => _AnimatedPositionedPageState();
}
class _AnimatedPositionedPageState extends State<AnimatedPositionedPage> {
bool flag = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AnimatedPositioned'),
),
body: Stack(
children: [
AnimatedPositioned(
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
top: flag ? 10 : 500,
left: flag ? 10 : 300,
child: Container(
width: 60,
height: 60,
color: Colors.green,
),
),
Align(
alignment: const Alignment(0, 0.8),
child: ElevatedButton(
onPressed: () {
setState(() {
flag = !flag;
});
},
child: const Text("Transform")),
),
],
),
);
}
}
4、AnimatedOpacity
属性
(new) AnimatedOpacity AnimatedOpacity({
Key? key,
Widget? child,
required double opacity, //透明度 0~1
Curve curve = Curves.linear, //动画运用的曲线
required Duration duration, //动画时长
void Function()? onEnd, //动画结束后回调
//是否总是包含语义信息,默认是 false。这个主要是用于辅助访问的,如果是 true,则不管透明度是多少,都会显示语义信息(可以辅助朗读),这对于视障人员来说会更友好。
bool alwaysIncludeSemantics = false,
})
使用
class AnimatedOpacityPage extends StatefulWidget {
const AnimatedOpacityPage({super.key});
@override
State<AnimatedOpacityPage> createState() => _AnimatedOpacityPageState();
}
class _AnimatedOpacityPageState extends State<AnimatedOpacityPage> {
bool flag = false;
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
flag = !flag;
});
},
child: const Icon(Icons.opacity),
),
appBar: AppBar(
title: const Text('AnimatedOpacity'),
),
body: Center(
child: AnimatedOpacity(
opacity: flag ? 0 : 1,
duration: const Duration(seconds: 3),
curve: Curves.linear,
child: Container(
width: 300,
height: 300,
color: Colors.green,
),
),
),
);
}
}
5、AnimatedDefaultTextStyle
属性
(new) AnimatedDefaultTextStyle AnimatedDefaultTextStyle({
Key? key,
required Widget child,
required TextStyle style, //子元素的样式,用于动画变化
TextAlign? textAlign, //如果文本超过1行时,所有换行的字体的对齐方式,可以是左对齐、右对齐
bool softWrap = true, //文本是否应该在软换行符处换行,软换行和硬换行是word用法,具体自阅
TextOverflow overflow = TextOverflow.clip, //超过文本行数区域的裁剪方式
int? maxLines, //文本最大行数,默认是1
TextWidthBasis textWidthBasis = TextWidthBasis.parent, //Text 宽度类型(与父 widget 同宽或最小宽度)
TextHeightBehavior? textHeightBehavior, //Text 行高状态
Curve curve = Curves.linear, //动画样式
required Duration duration, //动画时长
void Function()? onEnd, //动画结束后回调
})
使用
class AnimatedDefaultTextStylePage extends StatefulWidget {
const AnimatedDefaultTextStylePage({super.key});
@override
State<AnimatedDefaultTextStylePage> createState() =>
_AnimatedDefaultTextStylePageState();
}
class _AnimatedDefaultTextStylePageState
extends State<AnimatedDefaultTextStylePage> {
bool flag = false;
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.opacity),
onPressed: () {
setState(() {
flag = !flag;
});
}),
appBar: AppBar(
title: const Text('AnimatedDefaultTextStylePage'),
),
body: Center(
child: Container(
width: 300,
height: 300,
alignment: Alignment.center,
color: Colors.green,
child: AnimatedDefaultTextStyle(
style: TextStyle(fontSize: flag ? 15 : 20),
duration: const Duration(milliseconds: 300),
child: const Text("AnimatedDefaultTextStyle")),
),
),
);
}
}
6、AnimatedSwitcher(切换)
AnimatedContainer、AnimatedPadding、AnimatedPositioned、AnimatedOpacity、
AnimatedDefaultTextStyle 都是在属性改变的时候执行动画,AnimatedSwitcher 则是在子元素改变的时候执行动画。相比上面的动画组件 AnimatedSwitcher 多了 transitionBuilder 参数,可以在 transitionBuilder 中自定义动画。
属性
(new) AnimatedSwitcher AnimatedSwitcher({
Key? key,
Widget? child,
required Duration duration, // 新child显示动画时长
Duration? reverseDuration, // 旧child隐藏的动画时长
Curve switchInCurve = Curves.linear, // 新child显示的动画曲线
Curve switchOutCurve = Curves.linear, // 旧child隐藏的动画曲线
Widget Function(Widget, Animation<double>) transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // 动画构建器
Widget Function(Widget?, List<Widget>) layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, //布局构建器
})
使用
class AnimatedSwitcherPage extends StatefulWidget {
const AnimatedSwitcherPage({super.key});
@override
State<AnimatedSwitcherPage> createState() => _AnimatedSwitcherPageState();
}
class _AnimatedSwitcherPageState extends State<AnimatedSwitcherPage> {
bool flag = true;
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.opacity),
onPressed: () {
setState(() {
flag = !flag;
});
}),
appBar: AppBar(
title: const Text('AnimatedSwitcherPage'),
),
body: Center(
child: Container(
width: 300,
height: 180,
alignment: Alignment.center,
color: Colors.green,
child: AnimatedSwitcher(
duration: const Duration(seconds: 3),
child: flag
? const CircularProgressIndicator()
: Image.network(
"https://www.itying.com/images/flutter/2.png",
fit: BoxFit.cover,
),
),
),
),
);
}
}
//transitionBuilder 自定义动画
//在上述代码的 body 中的 AnimatedSwitcher 中添加 transitionBuilder 属性
body: Center(
child: Container(
width: 300,
height: 180,
alignment: Alignment.center,
color: Colors.green,
child: AnimatedSwitcher(
//transitionBuilder 自定义动画效果
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
duration: const Duration(seconds: 3),
child: flag
? const CircularProgressIndicator()
: Image.network(
"https://www.itying.com/images/flutter/2.png",
fit: BoxFit.cover,
),
),
),
),
//transitionBuilder 改变子元素执行动画
//在上述代码更改的基础上,更改 AnimatedSwitcher 中添加 child 属性
body: Center(
child: Container(
width: 300,
height: 180,
alignment: Alignment.center,
color: Colors.green,
child: AnimatedSwitcher(
//transitionBuilder 自定义动画效果
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
duration: const Duration(seconds: 3),
child: Text(
key: UniqueKey(),
flag ? "你好 Flutter" : "你好啊!",
style: const TextStyle(fontSize: 30),
),
),
),
),
3、显示动画
常见的显式动画有 RotationTransition(旋转)、FadeTransition(透明度)、ScaleTransition(缩放)、SlideTransition(移动)、AnimatedIcon(改变常见图标)。在显示动画中开发者需要创建一个 AnimationController,通过 AnimationController 控制动画的开始、暂停、重置、跳转、倒播等。
1、RotationTransition
属性
(new) RotationTransition RotationTransition({
Key? key,
required Animation<double> turns, //动画控制器
Alignment alignment = Alignment.center, //设置动画的旋转中心
FilterQuality? filterQuality, //在进行图像变换的过程中,图像的取样质量
Widget? child, //将要执行动画的子view
})
使用
class RotationTransitionPage extends StatefulWidget {
const RotationTransitionPage({super.key});
@override
State<RotationTransitionPage> createState() => _RotationTransitionPageState();
}
class _RotationTransitionPageState extends State<RotationTransitionPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
//Vsync 机制可以理解为是显卡与显示器的通信桥梁,显卡在渲染每一帧之前会等待垂直同步信号,只有显示器完成了一次刷新时,发出垂直同步信号,
//显卡才会渲染下一帧,确保刷新率和帧率保持同步,以达到供需平衡的效果,防止卡顿现象。
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('RotationTransition'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
RotationTransition(
turns: _controller,
child: const FlutterLogo(
size: 100,
),
),
const SizedBox(
height: 50,
),
Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
child: Wrap(
spacing: 10,
alignment: WrapAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_controller.forward();
},
child: const Text("正序播放")),
ElevatedButton(
onPressed: () {
_controller.reverse();
},
child: const Text("倒序播放")),
ElevatedButton(
onPressed: () {
_controller.stop();
},
child: const Text("停止播放")),
ElevatedButton(
onPressed: () {
_controller.reset();
},
child: const Text("重置")),
ElevatedButton(
onPressed: () {
_controller.repeat();
},
child: const Text("重复播放"))
],
),
)
],
),
);
}
}
lowerBound & upperBound
AnimationController 用于控制动画,它包含动画的启动 forward() 、停止 stop() 、反向播放reverse() 等方法。 AnimationController 会在动画的每一帧生成一个新的值。默认情况下, AnimationController 在给定的时间段内线性的生成从 0.0 到1.0(默认区间)的数字 ,我们也可以通过 lowerBound 和 upperBound 来修改 AnimationController 生成数字的区间。
//在上述代码中更改 initState() 方法如下:
void initState() {
super.initState();
//Vsync 机制可以理解为是显卡与显示器的通信桥梁,显卡在渲染每一帧之前会等待垂直同步信号,只有显示器完成了一次刷新时,发出垂直同步信号,
//显卡才会渲染下一帧,确保刷新率和帧率保持同步,以达到供需平衡的效果,防止卡顿现象。
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
//第三圈到第五圈
lowerBound: 3,
upperBound: 5,
);
_controller.addListener(() {
print(_controller.value);
});
}
2、FadeTransition
属性
(new) FadeTransition FadeTransition({
Key? key,
required Animation<double> opacity, //组件的透明度
//是否总是包含语义信息,默认是 false。这个主要是用于辅助访问的,如果是 true,则不管透明度是多少,都会显示语义信息(可以辅助朗读),这对于视障人员来说会更友好。
bool alwaysIncludeSemantics = false,
Widget? child,
})
使用
class FadeTransitionPage extends StatefulWidget {
const FadeTransitionPage({super.key});
@override
State<FadeTransitionPage> createState() => _FadeTransitionPageState();
}
class _FadeTransitionPageState extends State<FadeTransitionPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
//Vsync 机制可以理解为是显卡与显示器的通信桥梁,显卡在渲染每一帧之前会等待垂直同步信号,只有显示器完成了一次刷新时,
//发出垂直同步信号,显卡才会渲染下一帧,确保刷新率和帧率保持同步,以达到供需平衡的效果,防止卡顿现象。
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 3));
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('FadeTransition'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FadeTransition(
opacity: _controller,
child: const FlutterLogo(
size: 50,
),
),
const SizedBox(
height: 50,
),
Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
_controller.forward();
},
child: const Text("正序播放")),
ElevatedButton(
onPressed: () {
_controller.reverse();
},
child: const Text("倒序播放")),
ElevatedButton(
onPressed: () {
_controller.repeat();
},
child: const Text("重复播放"))
],
),
),
],
),
);
}
}
3、ScaleTransition
属性
(new) ScaleTransition ScaleTransition({
Key? key,
required Animation<double> scale, //动画控制器
Alignment alignment = Alignment.center, //设置动画的缩放中心
FilterQuality? filterQuality, //在进行图像变换的过程中,图像的取样质量
Widget? child, //将要执行动画的子view
})
使用
class ScaleTransitionPage extends StatefulWidget {
const ScaleTransitionPage({super.key});
@override
State<ScaleTransitionPage> createState() => _ScaleTransitionPageState();
}
class _ScaleTransitionPageState extends State<ScaleTransitionPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 3));
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ScaleTransitionPage'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ScaleTransition(
scale: _controller,
child: const FlutterLogo(
size: 100,
),
),
const SizedBox(
height: 50,
),
Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
_controller.forward();
},
child: const Text("正序播放")),
ElevatedButton(
onPressed: () {
_controller.reverse();
},
child: const Text("倒序播放")),
],
),
),
],
),
);
}
}
AnimationController 结合 Tween 控制动画
默认情况下, AnimationController 对象值的范围是[0.0,1.0]。如果我们需要构建 UI 的动画值在不同的范围或不同的数据类型,则可以使用 Tween 来添加映射以生成不同的范围或数据类型的值。
//上述代码中更改 ScaleTransition 如下:
ScaleTransition(
scale: _controller.drive(Tween(begin: 1,end: 2)),
child: const FlutterLogo(
size: 100,
),
),
4、SlideTransition
这是一负责平移的显示动画组件,使用时需要通过 position 属性传入一个 Animated 表示位移程度,通常借助 Tween 实现。
属性
(new) SlideTransition SlideTransition({
Key? key,
required Animation<Offset> position, //位置偏移系数(如:0.5 表示向右偏移 width 的 50%)
bool transformHitTests = true, //默认为 true。作用是否对 pointerEvent 的 position 进行转换
TextDirection? textDirection, //文本方向,一般都是从左到右
Widget? child,
})
使用
class SlideTransitionPage extends StatefulWidget {
const SlideTransitionPage({super.key});
@override
State<SlideTransitionPage> createState() => _SlideTransitionPageState();
}
class _SlideTransitionPageState extends State<SlideTransitionPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('SlideTransitionPage'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SlideTransition(
position: _controller.drive(Tween(
begin: const Offset(-1, -1),
//表示实际的位置向右移动自身宽度的1.2倍
end: const Offset(0.5, 0.5))),
child: const FlutterLogo(
size: 100,
),
),
const SizedBox(
height: 50,
),
Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
_controller.forward();
},
child: const Text("正序播放")),
ElevatedButton(
onPressed: () {
_controller.reverse();
},
child: const Text("倒序播放")),
],
),
),
],
),
);
}
}
Tween.animate 驱动动画
//如上代码更改 SlideTransition 代码块如下
SlideTransition(
// position: _controller.drive(Tween(
// begin: const Offset(-1, -1),
// //表示实际的位置向右移动自身宽度的1.2倍
// end: const Offset(0.5, 0.5))),
//Tween.animate 驱动动画
position:
Tween(begin: const Offset(-1, -1), end: const Offset(0.5, 0.5))
.animate(_controller),
child: const FlutterLogo(
size: 100,
),
),
链式操作修改动画效果
//如上代码更改 SlideTransition 代码块如下
SlideTransition(
// position: _controller.drive(Tween(
// begin: const Offset(-1, -1),
// //表示实际的位置向右移动自身宽度的1.2倍
// end: const Offset(0.5, 0.5))),
// Tween.animate 驱动动画
// position:
// Tween(begin: const Offset(-1, -1), end: const Offset(0.5, 0.5))
// .animate(_controller),
//链式操作修改动画效果
position:
Tween(begin: const Offset(0, -1), end: const Offset(0, 0.8))
.chain(CurveTween(curve: Curves.bounceIn))
.animate(_controller),
child: const FlutterLogo(
size: 100,
),
),
链式操作修改动动画执行时间
//如上代码更改 SlideTransition 代码块如下
SlideTransition(
// position: _controller.drive(Tween(
// begin: const Offset(-1, -1),
// //表示实际的位置向右移动自身宽度的1.2倍
// end: const Offset(0.5, 0.5))),
//Tween.animate 驱动动画
// position:
// Tween(begin: const Offset(-1, -1), end: const Offset(0.5, 0.5))
// .animate(_controller),
//链式操作修改动画效果
// position:
// Tween(begin: const Offset(0, -1), end: const Offset(0, 0.8))
// .chain(CurveTween(curve: Curves.bounceIn))
// .animate(_controller),
// 链式操作修改动动画执行时间
position:
Tween(begin: const Offset(0, -1), end: const Offset(0, 0.8))
.chain(CurveTween(curve: Curves.bounceIn))
//最后百分之 30 的时间完成动画
.chain(CurveTween(curve: const Interval(0.7, 1.0)))
.animate(_controller),
child: const FlutterLogo(
size: 100,
),
),
5、AnimatedIcon
AnimatedIcon 顾名思义,是一个用于提供动画图标的组件,它的名字虽然是以 Animated 开头,但是他是一个显式动画组件,需要通过 progress 属性传入动画控制器,另外需要由 Icon 属性传入动画图标数据。
属性
(new) AnimatedIcon AnimatedIcon({
Key? key,
required AnimatedIconData icon, //图标
required Animation<double> progress, //动画的进度,值0-1
Color? color, //icon 的颜色
double? size, //icon的大小
String? semanticLabel, //语义标签,不在UI中显示,在辅助功能模式下有用
TextDirection? textDirection, //图标的方向
})
方法
class AnimatedIconPage extends StatefulWidget {
const AnimatedIconPage({super.key});
@override
State<AnimatedIconPage> createState() => _AnimatedIconPageState();
}
class _AnimatedIconPageState extends State<AnimatedIconPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller.forward();
},
child: const Icon(Icons.add),
),
appBar: AppBar(
title: const Text('AnimatedIcon'),
),
body: Center(
child: AnimatedIcon(
icon: AnimatedIcons.menu_close,
progress: _controller,
size: 50,
),
),
);
}
}
4、交错动画
class SlideTransition2Page extends StatefulWidget {
const SlideTransition2Page({super.key});
@override
State<SlideTransition2Page> createState() => _SlideTransition2PageState();
}
class _SlideTransition2PageState extends State<SlideTransition2Page>
with SingleTickerProviderStateMixin {
bool flag = true;
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 6))
..repeat(reverse: true);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
flag ? _controller.forward() : _controller.reverse();
flag = !flag;
},
child: const Icon(Icons.refresh),
),
appBar: AppBar(
title: const Text('交错动画'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SlideringBox(
controller: _controller,
color: Colors.blue[200],
curve: const Interval(0, 0.2)),
SlideringBox(
controller: _controller,
color: Colors.blue[400],
curve: const Interval(0.2, 0.4)),
SlideringBox(
controller: _controller,
color: Colors.blue[600],
curve: const Interval(0.4, 0.6)),
SlideringBox(
controller: _controller,
color: Colors.blue[800],
curve: const Interval(0.6, 0.8)),
SlideringBox(
controller: _controller,
color: Colors.blue[900],
curve: const Interval(0.8, 1.0)),
],
),
),
);
}
}
class SlideringBox extends StatelessWidget {
final AnimationController controller;
final Color? color;
final Curve curve;
const SlideringBox(
{super.key,
required this.controller,
required this.color,
required this.curve});
@override
Widget build(BuildContext context) {
return SlideTransition(
position: Tween(begin: const Offset(0, 0), end: const Offset(0.7, 0))
.chain(CurveTween(curve: Curves.bounceIn)
.chain(CurveTween(curve: curve)))
.animate(controller),
child: Container(
width: 220,
height: 60,
color: color,
),
);
}
}
5、自定义动画
1、TweenAnimationBuilder 自定义隐式动画
每当 Tween 的 end 发生变化的时候就会触发动画。
属性
(new) TweenAnimationBuilder<Object?> TweenAnimationBuilder({
Key? key,
required Tween<Object?> tween, //动画值
required Duration duration, //动画时长
Curve curve = Curves.linear, //动效
//有三个参数,
//第一个是BuildContext,
//第二个是value用于接收上面两个参数定义的动画时间与动画值(类型取决于自己要做动画的数据类型),
//第三个是TweenAnimationBuilder的子组件,用于优化;
required Widget Function(BuildContext, Object?, Widget?) builder,
void Function()? onEnd, //动画结束后的回调
Widget? child,
})
使用
//大小变化
class TweenAnimationBuilderPage extends StatefulWidget {
const TweenAnimationBuilderPage({super.key});
@override
State<TweenAnimationBuilderPage> createState() =>
_TweenAnimationBuilderPageState();
}
class _TweenAnimationBuilderPageState extends State<TweenAnimationBuilderPage> {
bool flag = true;
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.refresh_sharp),
onPressed: () {
setState(() {
flag = !flag;
});
}),
appBar: AppBar(
title: const Text('TweenAnimationBuilder'),
),
body: Center(
child: TweenAnimationBuilder(
tween: Tween(
begin: 100.0, end: flag ? 100.0 : 200.0), //此处的数据必须为 double 类型数据
duration: const Duration(milliseconds: 100),
builder: ((context, value, child) {
return Icon(
Icons.star,
size: value.toDouble(),
);
})),
),
);
}
}
//透明度变化
//修改上述 body 中的代码,如下
//大小变化
// body: Center(
// child: TweenAnimationBuilder(
// tween: Tween(
// begin: 100.0, end: flag ? 100.0 : 200.0), //此处的数据必须为 double 类型数据
// duration: const Duration(milliseconds: 100),
// builder: ((context, value, child) {
// return Icon(
// Icons.star,
// size: value.toDouble(),
// );
// })),
// ),
//透明度变化
body: Center(
child: TweenAnimationBuilder(
tween: Tween(begin: 0.0, end: flag ? 0.2 : 1.0),
duration: const Duration(milliseconds: 100),
builder: ((context, value, child) {
return Opacity(
opacity: value,
child: Container(
color: Colors.red,
width: 200,
height: 200,
),
);
})),
),
2、AnimatedBuilder 自定义显式动画
属性
(new) AnimatedBuilder AnimatedBuilder({
Key? key,
required Listenable animation, //用于监听该动画,然后通知更新UI
required Widget Function(BuildContext, Widget?) builder, //构建动画
Widget? child,
})
使用
透明度动画
class AnimatedBuilderPage extends StatefulWidget {
const AnimatedBuilderPage({super.key});
@override
State<AnimatedBuilderPage> createState() => _AnimatedBuilderPageState();
}
class _AnimatedBuilderPageState extends State<AnimatedBuilderPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1))
..repeat(reverse: true);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AnimatedBuilder'),
),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return Opacity(
opacity: _controller.value,
child: Container(
width: 200,
height: 200,
color: Colors.red,
child: const Center(
child: Text("AnimatedBuilder"),
)
),
);
})),
);
}
}
自定义变化范围
//修改上述 body 中的代码,如下
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
///透明度动画
// return Opacity(
// opacity: _controller.value,
// child: Container(
// width: 200,
// height: 200,
// color: Colors.red,
// child: const Center(
// child: Text("AnimatedBuilder"),
// )
// ),
// );
///自定义变化范围
return Opacity(
opacity:
Tween(begin: 0.4, end: 1.0).animate(_controller).value,
child: Container(
width: 200,
height: 200,
color: Colors.red,
child: const Center(
child: Text("AnimatedBuilder"),
)),
);
})),
位置变化
//将上述 body 中的代码更改如下
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: 200,
height: 200,
color: Colors.red,
transform: Matrix4.translationValues(
Tween(begin: -100.0, end: 100.0)
.chain(CurveTween(curve: Curves.bounceIn))
.chain(CurveTween(curve: const Interval(0.2, 0.8)))
.animate(_controller)
.value,
0,
0),
child: const Text("AnimatedBuilder"),
);
}),
),
chilid优化
//将上述 body 中的代码更改如下
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: 200,
height: 200,
color: Colors.red,
transform: Matrix4.translationValues(
Tween(begin: -100.0, end: 100.0)
.chain(CurveTween(curve: Curves.bounceIn))
.chain(CurveTween(curve: const Interval(0.2, 0.8)))
.animate(_controller)
.value,
0,
0),
child: child,
);
},
child: const Text("AnimatedBuilder"),
),
),
6、Hero 动画
1、介绍
微信朋友圈点击小图片的时候会有一个动画效果到大图预览,这个动画效果就可以使用 Hero 动画实现。Hero 指的是可以在路由(页面)之间飞行
的 widget,简单来说 Hero 动画就是在路由切换时,有一个共享的 widget 可以在新旧路由间切换。
2、属性
(new) Hero Hero({
Key? key,
//用于关联两个两个界面的 Hero 组件,两个 Hero 组件有关联关系 , 则设置相同的 tag 字符串
required Object tag,
//用于定义 Hero 组件的边界 , 以及定义 Hero 组件在界面切换时,从源界面的起始位置到目的界面的最终位置 , 动画执行的变化过程
Tween<Rect?> Function(Rect?, Rect?)? createRectTween,
//动画过程组件
Widget Function(BuildContext, Animation<double>, HeroFlightDirection, BuildContext, BuildContext)? flightShuttleBuilder,
//占位符组件
Widget Function(BuildContext, Size, Widget)? placeholderBuilder,
//使用手势进行转场时,是否显示动画
bool transitionOnUserGestures = false,
//普通的 Widget 组件 , Hero 动画作用的组件
required Widget child,
})
3、使用
//HomePage
import 'package:flutter/material.dart';
import 'package:terminalflutter01/listData.dart';
import './HeroPage.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
///数据源
final List<Widget> _getListData = [];
@override
void initState() {
super.initState();
for (var i = 0; i < listData.length; i++) {
_getListData.add(
///手势
GestureDetector(
onTap: () {
//添加舔砖图片链接
Navigator.push(context, MaterialPageRoute(builder: (context) {
return HeroPage(arguments: {
"imageUrl": listData[i]['imageUrl'],
"description": "$i",
});
}));
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: const Color.fromRGBO(233, 233, 233, 0.9), width: 1)),
child: Column(
children: [
Hero(
tag: listData[i]['imageUrl'],
child: Image.network(listData[i]['imageUrl'])),
const SizedBox(height: 12),
Text(
listData[i]['title'],
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 15),
)
],
),
),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('HomePage'),
),
body: GridView.count(
crossAxisSpacing: 10, //子组件水平间距
mainAxisSpacing: 10, //子组件垂直间距
crossAxisCount: 2, //子组件水平数量
padding: const EdgeInsets.all(10),
children: _getListData,
),
);
}
}
//HeroPage
import 'package:flutter/material.dart';
class HeroPage extends StatefulWidget {
final Map arguments;
const HeroPage({super.key, required this.arguments});
@override
State<HeroPage> createState() => _HeroPageState();
}
class _HeroPageState extends State<HeroPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('HeroPage'),
),
body: ListView(
children:[
Hero(
tag: widget.arguments["imageUrl"],
child: Image.network(widget.arguments["imageUrl"])
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.all(5),
child: Text(
widget.arguments["description"],
style: const TextStyle(fontSize: 22)
),
)
],
),
);
}
}
注:可以通过第三方 photo_view 实现单张图片、多张图片预览
补充:Curves 曲线值
参考:
Curves 效果视图 >>>
Curves 效果视图(背用1) >>>
Curves 效果视图(背用2) >>>