跟🤡杰哥一起学Flutter ——玩转Flutter动画[上]
🤡 在 《十一、Flutter UI框架🦐聊》提到过 Flutter 的本质是一套「UI框架」,解决的是「一套代码在多端的渲染」。写 UI 时除了常规的 堆Widget 外,适当加点 动画,可以让我们的 App 变得很 炫,本节我们就来系统学习下Flutter中 动画 相关的姿势~
😄 通常会把这个静止图像称为「帧」,它是影像中的 最小单位,而平时提到的60帧、90帧、120帧指的是 帧率(FPS,Frame Per Second) ,即 一秒钟有多少个"画面"在屏幕上发生刷新。60帧就是1s刷新60次,每帧的持续时间约为16.67ms:
3.2. Curve-动画曲线
抽象类,继承自 ParametricCurve ,用于定义 参数化动画缓动曲线 (动画随时间的变化速率) 。我们把 匀速动画 称为 线性 的,非匀速动画 称为 非线性 的。一般很少 自定义Curve (重写transform方法,实现自定义插值逻辑),而是使用Flutter 给我们提供的 预定义曲线,常用曲线 (通过 Curves 类访问,如Curves.linear ):
| 缓动曲线名称 | 描述 |
|---|---|
| linear | 线性曲线,匀速变化 |
| decelerate | 开始较快,然后减速,倒置的 f(t) = t² 抛物线 |
| ease | 立方贝塞尔曲线,开始和结束时缓慢,中间加速。 |
| easeIn | 立方贝塞尔曲线,开始时缓慢,然后加速。 |
| easeOut | 立方贝塞尔曲线,开始时加速,然后减速。 |
| easeInOut | 立方贝塞尔曲线,开始和结束时缓慢,中间加速。 |
| fastOutSlowIn | 立方贝塞尔曲线,开始时快速,然后减速。 |
| bounceIn | 振荡曲线,幅度逐渐增大。 |
| bounceOut | 振荡曲线,幅度逐渐减小。 |
| elasticIn | 振荡曲线,幅度逐渐增大,同时超出其边界。 |
| elasticOut | 振荡曲线,幅度逐渐减小,同时超出其边界。 |
3.3. AnimationController-动画控制器
用于 控制动画的播放、状态和进度,继承于 Animation ,常用属性:
value → T,当前动画的值。 duration → Duration,动画的持续时间。 reverseDuration → Duration,动画反向播放的持续时间。 lowerBound → double,动画的最小值,默认为0.0。 upperBound → double,动画的最大值,默认为1.0。 status → AnimationStatus,动画的当前状态。 velocity → double,动画值每秒的变化率。 isAnimating → bool,动画是否正在播放。 lastElapsedDuration → Duration,动画开始到最近一次动画更新经过的时间。
常用方法:
forward({ double? from }):从当前值或指定值开始向前播放动画。 reverse({ double? from }):从当前值或指定值开始反向播放动画。 animateTo(double target, { Duration? duration, Curve curve = Curves.linear }):将动画从当前值驱动到目标值。 animateBack(double target, { Duration? duration, Curve curve = Curves.linear }):将动画从当前值反向驱动到目标值。 repeat({ double? min, double? max, bool reverse = false, Duration? period }):重复播放动画,可以指定最小值、最大值、是否反向和周期。 fling({ double velocity = 1.0, SpringDescription? springDescription, AnimationBehavior? animationBehavior }):使用弹簧模拟驱动动画(阻尼效果)。 animateWith(Simulation simulation):根据给定的模拟驱动动画。 stop({ bool canceled = true }):停止动画。 dispose():释放动画控制器使用的资源。 reset():将控制器的值设置为 lowerBound,停止动画并重置到起点或解散状态。 resync(TickerProvider vsync):使用新的 TickerProvider 重新创建 Ticker。
3.4. Tween-补间动画
Animatable 的子类,用于在动画中插入两个值(begin & end),并在动画生命周期内生成一系列值。常用方法:
transform(double t):抽象方法,子类实现,根据传入的动画时钟值t (动画过程变化的值,通常是0.0-1.0 之间) 返回计算后的插值值。 evaluate(Animationanimation):使用动画的当前值来计算插值值,通过调用 transform() 来实现。 animate() :将 Tween 对线应用到 Animation 对象上,生成插值值。 chain() :将多个 Tween 对象链接在一起,使得 Animation 对象可以依次使用它们生成插值值。
4. 🌰 写下例子
4.1. 实现Widget缩放动画
接着写下例子,循序渐进了解这个4个核心API的作用,先整个Container从小变大的动画吧:
class AnimatedBox extends StatefulWidget {
const AnimatedBox({super.key}); @override
State createState() => _AnimatedBoxState();
}
// 💡 混入 SingleTickerProviderStateMixin 以获取 vsync
class _AnimatedBoxState extends State<AnimatedBox> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
// 💡 初始化 AnimationController,设置动画时长为2秒
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
// 💡 初始化 Tween 补间动画,设置起始值为0,结束值为300,添加值变化监听,setState() 刷新UI
_animation = Tween(begin: 0.0, end: 200.0).animate(_controller)..addListener(() {
setState(() {});
});
// 启动动画(正向执行)
_controller.forward();
}
@override
void dispose() {
// 💡 释放 AnimationController
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 💡 通过 Animation.value 获取当前动画值,设置宽高
return Container(
width: _animation.value,
height: _animation.value,
color: Colors.blue,
);
}
}
运行看下效果:
4.2. 开启无限循环
🤔 em... 动画只执行一次就停止了,怎么让它无限循环呢?一个简单的粗暴的方法:添加动画状态监听,在动画结束后,重置动画,然后重新启动动画:
_animation.addStatusListener((status) {
// 💡 监听动画结束后,重置动画,并重新启动
if (status == AnimationStatus.completed) {
_controller.reset();
_controller.forward();
}
});
运行效果如下:
4.3. 循环变大变小
🤔 em... 实现下 从小变大又从大变小 呢?修改下动画状态监听,正向结束反向执行,反向结束正向执行:
_animation.addStatusListener((status) {
// 💡 动画正向执行结束后,反向执行, 反向执行结束后,正向执行
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
运行效果如下:
😏 其实,实现上面的效果,只需要调用一句 _controller.repeat(reverse: true); 来启动动画即可。
4.4. 修改动画曲线
从动画效果不难看出 Tween 默认的 动画曲线 是线性的,即动画的值会以恒定的速度从起始值变化到结束值。接着试下把动画曲线改为 CurvedAnimation,并将其设置为 Curves.easeInOut → 中间快,开始和结束慢:
_animation = Tween(begin: 0.0, end: 200.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
运行效果如下:
😄 感觉这个动画曲线不够好玩,继承 Curve,重写 transform() 实现一个 阻尼效果 吧:
class DampingCurve extends Curve {
@override
double transform(double t) {
// 💡 实现阻尼效果的曲线逻辑
return (1 - (1 - t) * (1 - t));
}
}// 调用处:
_animation = Tween(begin: 0.0, end: 200.0).animate(
CurvedAnimation(parent: _controller, curve: DampingCurve())
);
运行效果如下:
4.5. 多种动画并行
😄 单纯的变大变小还不够,再加个旋转试试?
class _AnimatedBoxState extends State<AnimatedBox> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<double> _rotationAnimation; @override
void initState() {
super.initState();
// 💡 初始化 AnimationController,设置动画时长为2秒
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
// 💡 大小变化动画
_sizeAnimation = Tween(begin: 0.0, end: 200.0).animate(CurvedAnimation(parent: _controller, curve: DampingCurve()))
..addListener(() {
setState(() {});
});
// 💡 旋转变化动画
_rotationAnimation = Tween(begin: 0.0, end: 2 * pi).animate(_controller)
..addListener(() {
setState(() {});
});
// 💡 启动动画
_controller.repeat(reverse: true);
}
@override
void dispose() {
// 💡 释放 AnimationController
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: _rotationAnimation.value,
child: Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
color: Colors.blue,
),
);
}
}
运行效果如下:
😁 有点意思,要不再加个 从底下往上抛的动画?在添加之前,问读者一个问题:
觉不觉得每新增一个动画,都要写一遍 addListener(() { setState(() {}); }) 有些麻烦?
😏 这个,可以用 Flutter 提供的动画便捷构建工具 → AnimatedBuilder 来解决,它允许我们将动画与UI逻辑分离,从而简化代码并提高可读性。加上上抛动画的代码如下:
// 💡 分别定义缩放、旋转、上抛动画
late Animation<double> _sizeAnimation;
late Animation<double> _rotationAnimation;
late Animation<Offset> _parabolicAnimation;// 💡 初始化动画,此时不需要再 addListener(() { setState(() {}); })
_sizeAnimation = Tween(begin: 0.0, end: 200.0).animate(CurvedAnimation(parent: _controller, curve: DampingCurve()));
_rotationAnimation = Tween(begin: 0.0, end: 2 * pi).animate(_controller);
_parabolicAnimation = Tween<Offset>(begin: const Offset(0, 300), end: const Offset(0, -200))
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
// 💡 AnimatedBuilder 只会再动画更新是重建其子树
@override
Widget build(BuildContext context) {
return AnimatedBuilder(animation: _controller, builder: (context, child) {
return Transform.translate(
offset: _parabolicAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value,
child: Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
color: Colors.blue,
),
),
);
});
}
运行效果如下:
😄 还可以结合 Interval 定义 动画在整个持续时间内的特定时间段 来实现 交织动画 (Stagger Animation)。比如实现这样的效果:缩放动画持续运行、旋转动画在前半段时间内运行、位移动画在后半段时间内运行:
// 💡 缩放动画全程运行
_sizeAnimation = Tween(begin: 0.0, end: 200.0).animate(CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 1.0, curve: DampingCurve()),
));
// 💡 旋转动画在前半段时间运行,即0-1s
_rotationAnimation = Tween(begin: 0.0, end: 2 * pi).animate(CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.linear),
));
// 💡 位移动画在前半段时间运行,即1-2s
_parabolicAnimation =
Tween<Offset>(begin: const Offset(0, 200), end: const Offset(0, -200)).animate(CurvedAnimation(
parent: _controller,
curve: const Interval(0.5, 1.0, curve: Curves.easeInOut),
));
运行效果如下:
🤷♀️ 当然,如果不想用 Interval 和 Tween 来精确控制每个动画的时间段,也可以为每个动画单独定义一个 AnimationController 进行管理。AnimatedBuilder 的 animation 这样传下动画控制器实例就好:
AnimatedBuilder(animation: Listenable.merge([_sizeController, _rotationController, _parabolicController])
不过,当其中 任意一个AnimationController发生变化,AnimatedBuilder 都会重新构建其子树,在某些场景,为了避免不必要的刷新,可以将不同的动画分离到不同的 AnimatedBuilder 中。
5. 其它 API
5.1. Ticker
Flutter 中用于 驱动动画 的一个类,它会在每一帧调用一个回调函数,从而实现 动画的逐帧更新。 创建 AnimationController 是需要传递一个 vsync 参数,它接受一个 TickerProvider 类型的对象,它的主要职责是创建 Ticker。通常会将 SingleTickerProviderStateMixin 添加到 State 的定义中,然后将 State 对象作为 vsync 参数的值。提供 单个Ticker 用 SingleTickerProviderStateMixin,提供 多个Ticker 用 TickerProviderStateMixin。 Flutter 应用在启动时会绑定一个 SchedulerBinding 来给 每一次屏幕刷新添加回调,Ticker 通过它注册了帧回调。相比起直接用 Timer 来驱动动画,它可以防止屏幕外 (手机锁屏或应用切到后台) 继续消耗资源。
5.2. AnimationStatus-动画状态
用于表示动画的当前状态的 枚举类,包含以下四个枚举值:
dismissed:动画处于起始状态,且未开始播放。 forward: 动画正在从起始状态向结束状态播放。 reverse: 动画正在从结束状态向起始状态播放。 completed: 动画已到达结束状态。
5.3. lerp 函数
线性插值(Linear Interpolation)的缩写,用于在两个值之间进行插值,生成平滑的过渡效果。Flutter 中给有可能做动画的状态属性都定义了 静态的lerp()方法,如:
// a 为起始颜色,b 为终止颜色,t 为当前动画的进度 [0,1]
Color.lerp(a, b, t);
// 矩形
Rect.lerp(a, b, t);
// 大小
Size.lerp(a, b, t);
// 偏移
Offset.lerp(a, b, t);
// 对齐方式
Alignment.lerp(a, b, t);
// 文本样式
TextStyle.lerp(a, b, t);
// 边框半径
BorderRadius.lerp(a, b, t);
// a起始边框,b 为终止边框
Border.lerp(a, b, t);
// 盒子约束 (最大最小宽高)
BoxConstraints.lerp(a, b, t);
// 边距
EdgeInsets.lerp(a, b, t);
// 矩阵
Matrix4.lerp(a, b, t);
// 半径
Radius.lerp(a, b, t);
// 形状边框
ShapeBorder.lerp(a, b, t);
计算公式一般遵循:返回值 = a + (b - a) * t 。写个简单的颜色插值的简单示例:
class ColorLerpExample extends StatefulWidget {
@override
_ColorLerpExampleState createState() => _ColorLerpExampleState();
}class _ColorLerpExampleState extends State<ColorLerpExample> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
// 💡 使用 Color.lerp() 方法在红色和蓝色之间进行插值
Color color = Color.lerp(Colors.red, Colors.blue, _controller.value)!;
return Container(
color: color,
width: 100,
height: 100,
);
},
);
}
}
void main() {
runApp(MaterialApp(
home: Scaffold(
body: Center(
child: ColorLerpExample(),
),
),
));
}
运行效果如下:
当然,也可以直接利用 AnimationController#drive() + ColorTween 来实现颜色插值:
_colorAnimation = _controller.drive(
ColorTween(
begin: Colors.red,
end: Colors.blue,
),
);
点开 ColorTween 源码内部,其实还是调用的 Color.lerp() :
5.4. 显/隐式动画
显式动画 对应 AnimatedWidget,隐式动画 对应 ImplicitlyAnimatedWidget,前者:
需要显式传递一个 Listenable (通常是 Animation),手动管理 AnimationController 的生命周期。但也更灵活,可以使用 Tween 或 Curve 进行变换,适用于需要复杂动画控制的场景。设计它的初衷都是为了 减少样板代码,不需要在每次动画变化时 手动调用setState() ,简化动画处理。
后者:
会在内部创建和管理 AnimationController,开发者只需设置 目标值、持续时间 和 动画曲线。
5.4.1. ☀️ AnimatedWidget 的子类们
ListenableBuilder:监听器构建器,用于监听某个对象的变化并重建界面。 AnimatedBuilder:动画构建器,用于监听动画对象的变化并重建界面。 AlignTransition:对齐动画。 DecoratedBoxTransition:装饰动画。 DefaultTextStyleTransition:默认文本样式动画。 PositionedTransition:定位动画。 RelativePositionedTransition:相对定位动画。 RotationTransition:旋转动画。 ScaleTransition:缩放动画。 SizeTransition:大小动画。 SlideTransition:滑动动画。 FadeTransition:淡入淡出动画。 AnimatedModalBarrier:模态屏障动画。
接着用 AnimatedWidget 的子类们实现🌰写个例子处的动画效果,问题来了:缩放 有 ScaleTransition,旋转 有 RotationTransition,那 平移 呢?没有,🤷♀️ 那就自己写一个了吧:
class TranslateTransition extends AnimatedWidget {
// 💡 传入一个 Animation<Offset> 来控制平移效果
const TranslateTransition({
super.key,
required this.offset,
this.child,
}) : super(listenable: offset); // 💡 控制平移偏移量的动画
final Animation<Offset> offset;
final Widget? child;
@override
Widget build(BuildContext context) {
// 💡 将偏移动画的值应用到子组件上
return Transform.translate(
offset: offset.value,
child: child,
);
}
}
调用处代码:
class _AnimatedBoxState extends State<AnimatedBox> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation; @override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
_animation = _controller; // 默认动画曲线是线性的,有需要可以创建CurvedAnimation使用不同的动画曲线
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TranslateTransition(
// 💡 创建一个 Tween 插值器,指定偏移动画的开始值和结束值,调用 animate() 转换为 Animation 对象
offset: Tween<Offset>(
begin: const Offset(0, 300),
end: const Offset(0, -200),
).animate(_animation),
child: ScaleTransition(
scale: _animation,
child: RotationTransition(
turns: _animation,
child: Container(
color: Colors.blue,
width: 300,
height: 300,
),
),
),
);
}
}
😄 然后就实现了例子里同样的动画效果啦~
5.4.2. 🌙 ImplicitlyAnimatedWidget 的子类们
TweenAnimationBuilder:补间动画构建器,用于将任何由补间表达的属性动画化到指定的目标值。 AnimatedAlign:对齐动画。 AnimatedContainer:容器动画。 AnimatedDefaultTextStyle:默认文本样式动画。 AnimatedScale:缩放动画。 AnimatedRotation:旋转动画。 AnimatedSlide:滑动动画。 AnimatedOpacity:透明度动画。 AnimatedPadding:内边距动画。 AnimatedPhysicalModel:物理模型动画。 AnimatedPositioned:定位动画。 AnimatedPositionedDirectional:方向定位动画。 AnimatedTheme:主题动画。 AnimatedCrossFade:交叉淡入淡出动画。 AnimatedSize:大小动画。 AnimatedSwitcher:切换动画,在多个子组件之间进行淡入淡出动画。
😄 ImplicitlyAnimatedWidget 的子类都是以 Animated 开头的啊,同样用它的子类们实现🌰写个例子处的动画效果,同样 缩放 有 AnimatedScale,旋转 有 AnimatedRotation,平移 得自己写🤷♀️:
class AnimatedTranslate extends ImplicitlyAnimatedWidget {
const AnimatedTranslate({
super.key,
required this.offset,
required super.duration,
this.child,
}); final Offset offset;
final Widget? child;
@override
AnimatedWidgetBaseState createState() => _AnimatedTranslateState();
}
class _AnimatedTranslateState extends AnimatedWidgetBaseState<AnimatedTranslate> {
// 💡 用于平移的补间动画
Tween<Offset>? _offsetTween;
// 💡 重写此方法,遍历当前补间动画,检查是否需要创建新的补间动画或更新现有的补间动画,返回新的补间动画
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_offsetTween = visitor(
_offsetTween,
widget.offset,
(dynamic value) => Tween<Offset>(begin: value as Offset),
) as Tween<Offset>?;
}
@override
Widget build(BuildContext context) {
// 💡 根据 Animation<double> 对象的当前值,计算出对应的插值结果
return Transform.translate(
offset: _offsetTween?.evaluate(animation) ?? Offset.zero,
child: widget.child,
);
}
}
然后 隐式动画 无法主动控制动画的开始和暂停,必须重建组件才能执行动画,是的,需要手动触发 setState() ❗️ 这里加个按钮点击触发动画值的变化,具体代码如下:
class _AnimatedBoxState extends State<AnimatedBox> with SingleTickerProviderStateMixin {
final Duration _animationDuration = const Duration(seconds: 2); // 动画的持续时间
double _animationValue = 0.0; // 动画的起始值
Offset _offset = const Offset(0, 300); // 平移的偏移量 @override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: () {
setState(() {
// 💡 点击按钮时,切换动画的起始值和平移的偏移量
_animationValue = _animationValue == 0.0 ? 1.0 : 0.0;
_offset = _offset == const Offset(0, 300) ? const Offset(0, 0) : const Offset(0, 300);
});
},
child: const Text('开始动画'),
),
const SizedBox(height: 20),
AnimatedTranslate(
offset: _offset,
duration: _animationDuration,
child: AnimatedScale(
scale: _animationValue,
duration: _animationDuration,
child: AnimatedRotation(
turns: _animationValue,
duration: _animationDuration,
child: Container(
color: Colors.blue,
width: 300,
height: 300,
)),
))
],
);
}
}
代码运行效果 (快速点击按钮还会切换动画的执行方向🤣):
6. 有动画效果的组件
😊 限于篇幅,只做简单介绍,感兴趣的读者可自行编写Demo测试~
AnimatedIcon:显示一个带有动画效果的图标。 CircularProgressIndicator:显示一个圆形的进度指示器,表示任务正在进行中。 RefreshProgressIndicator:显示一个圆形的刷新进度指示器,通常用于下拉刷新操作。 LinearProgressIndicator:显示一个线性的进度条,表示任务的完成进度。 CupertinoActivityIndicator:显示一个iOS风格的旋转加载指示器。 RefreshIndicator:实现下拉刷新功能的组件,通常包裹在可滚动的列表外层。 Dismissible:实现滑动删除功能的组件,通常用于列表项。 FlutterLogo:显示Flutter的标志图标。 DrawerHeader:显示在抽屉顶部的头部区域,通常用于显示用户信息或应用标题。 Stepper:显示一个步骤指示器,通常用于多步骤的表单或流程。 ExpandIcon:显示一个可展开或收起的图标,通常用于折叠面板。
7. 路由动画
😄 这个在上节《二十二、玩转Flutter路由之——Navigator 1.0详解✈️》已经说过了,就不赘述了~
8. Hero动画
用于 在两个路由(页面) 切换时,平滑地移动一个共享的元素,让用户感觉到两个页面间的 视觉连续性。它的实现依赖于 Hero 和 Navigator,当用户导航到新页面时,Hero 组件会在两个页面间创建一个动画过渡。玩法很简单,只需要在两个页面使用 tag相同的Hero组件,Flutter会自动生成过渡帧,使用代码示例如下:
class FirstPage extends StatelessWidget {
const FirstPage({super.key}); @override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('First Page')),
body: Center(
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SecondPage()),
);
},
child: Hero(
tag: 'hero-tag',
child: Align(
alignment: Alignment.topLeft,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
),
),
);
}
}
class SecondPage extends StatelessWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Second Page')),
body: Center(
child: Hero(
tag: 'hero-tag',
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
);
}
}
运行效果如下:
9. 物理模拟动画
物理模拟能够让应用富有真实感和更好的交互性,Flutter 中提供了一系列的 Simulation 类来帮帮助我们实现物理模拟动画效果,常用的 Simulation 类:
GravitySimulation: 重力加速度。 FrictionSimulation: 摩擦力。 SpringSimulation: 弹簧振动。 BouncingScrollSimulation: 滚动视图边界反弹。 ScrollSpringSimulation:滚动视图的弹性滚动。 ClampingScrollSimulation: 滚动视图的夹紧滚动。 ClampedSimulation: 对另一个模拟应用限制,限制其输出的最小值和最大值。
😄 懒得想例子了,直接搬运下 官网 给出的Demo:实现一个可拖动的卡片组件,用户拖动卡片并释放时,卡片会使用弹簧效果回到屏幕中心,具体代码如下:
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';void main() {
runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}
class PhysicsCardDragDemo extends StatelessWidget {
const PhysicsCardDragDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const DraggableCard(
child: FlutterLogo(
size: 64,
),
),
);
}
}
// 一个支持拖拽,且松手后会回到中心位置的卡片
class DraggableCard extends StatefulWidget {
const DraggableCard({required this.child, super.key});
final Widget child;
@override
State<DraggableCard> createState() => _DraggableCardState();
}
class _DraggableCardState extends State<DraggableCard> with SingleTickerProviderStateMixin {
late AnimationController _controller;
Alignment _dragAlignment = Alignment.center; // 卡片的对齐方式
late Animation<Alignment> _animation; // 动画
void _runAnimation(Offset pixelsPerSecond, Size size) {
// 创建一个动画,从当前位置移动到中心位置
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
// 计算每秒的单位移动量
final unitsPerSecondX = pixelsPerSecond.dx / size.width;
final unitsPerSecondY = pixelsPerSecond.dy / size.height;
final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
final unitVelocity = unitsPerSecond.distance;
// 定义弹簧动画的参数
const spring = SpringDescription(
mass: 30,
stiffness: 1,
damping: 1,
);
// 创建弹簧动画模拟
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
// 使用弹簧动画来驱动控制器
_controller.animateWith(simulation);
}
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
// 添加监听器,当动画值发生变化时,更新_dragAlignment的值,刷新UI
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return GestureDetector(
// 当用户按下时,停止动画
onPanDown: (details) {
_controller.stop();
},
// 当用户拖动时,更新卡片位置
onPanUpdate: (details) {
setState(() {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 2),
details.delta.dy / (size.height / 2),
);
});
},
// 当用户松手时运行弹簧动画
onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
},
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
);
}
}
代码运行效果:
10. 小结
🤡 本节,杰哥带着大伙系统学习了Flutter中动画相关的API,算是对Flutter动画体系的整体有了一个基础的认识。相信你已经知道如何快速创建一个简单动画,以及控制动画的播放,但想要灵活自如运地使用动画,还需要大量的练习、实践与借鉴优秀作品~
参考文献:
《Flutter实战·第二版:Flutter动画简介》 《掘金小册:Flutter 跨平台开发实战》 《Flutter 工程化框架选择——搞定 Flutter 动画》 《flutter 中的动画详解》 点击公众关注号,“技术干货”及时达!