跟🤡杰哥一起学Flutter ——玩转Flutter动画[上]
点击公众关注号,“技术干货”及时达!1. 引言?? 在 《十一、Flutter UI框架??聊》提到过 Flutter 的本质是一套「UI框架」,解决的是「一套代码在多端的渲染」。写 UI 时除了常规的 堆Widget 外,适当加点 动画,可以让我们的 App 变得很 炫,本节我们就来系统学习下Flutter中 动画 相关的姿势~
本节学习大纲如下:
2. 概念名词2.1.1. 动画 (Animation) & 帧率 (PFS)?? 动画是什么?
动画 (Animation) 是一种通过定时拍摄一系列静止的 固态图像 (帧) 以 一定频率 连续变化、运动 (播放) 的速度(如每秒16张) 而导致肉眼的 视觉残像 产生的错觉——而误以为图画或物体 (画面) 活动的作品及其视频技术。——摘自wiki百科
概括下就是:
一系列 静态图像,以一定速度连续播放,利用人眼的视觉残像,创造出图像似乎在运动的技术。
?? 通常会把这个静止图像称为「帧」,它是影像中的 最小单位,而平时提到的60帧、90帧、120帧指的是 帧率(FPS,Frame Per Second) ,即 一秒钟有多少个"画面"在屏幕上发生刷新。60帧就是1s刷新60次,每帧的持续时间约为16.67ms:
接着看下不同帧率下的动画小姑,以 6帧小球图像 为例 (PFS分别为:2帧、10帧、16帧、60帧):
?? 2帧卡成PPT,10帧流畅了不少,但还是能察觉到一丝卡顿,16帧很丝滑,60帧反而有些鬼畜 (突兀)。
?? 问:不是FPS值越大,动画就越流畅吗?怎么这里60帧的反而让人感到不连贯?
?? 答:要创建流畅的动画或视频,不仅需要高FPS,还需要确保 每一帧间的内容变化适度。
上面这种通过 快速切换一系列静态图像来创建动画效果 的方式也叫「帧动画」,常见帧率简介:
16帧:早期电影和动画的标准帧率,动态效果略显生硬,基本能看。24帧:传统电影和电视剧,既能保证动画的流畅性,又能最大限度地降低胶片的使用量,有些还会使用"运动模糊"的技术,通过在每个帧间添加一些模糊,使得动作看起来更连贯。30帧:电视广播和一些不太需要快速反应的电子游戏,能够提供良好的观看体验。48帧:新的电影制作,特别是3D电影,更流畅的动作和更少的运动模糊。60帧:高清电视广播和大多数现代电子游戏,提供非常流畅的动作,大多数游戏的理想帧率。90帧:高端电脑游戏、部分VR设备,画面极为流畅,能充分展现高速运动的细节。120帧:高端电脑游戏、专业体育赛事直播、部分电影,提供极致流畅和细节丰富的视觉体验。240帧及以上:专业电竞,高速摄影,科研应用,能捕捉到极为细微的动作变化,240帧是人眼所能感知到的理论极限帧率。2.1.2. 过渡 (Transition)从一个状态或场景切换到另一个状态或场景 的动画效果,App 中常见的过渡类型有:页面过渡 (页面切换)、元素过渡 (如按钮未按下到按下)、列表过渡 (列表增删和重排序时的效果)、自定义过渡。
2.1.3. 缓动 (Easing)一种用于 模拟显示世界中物体运动的自然规律 的动画技术,通常通过特定的 缓动曲线(数学函数-描述动画随时间变化的速度) 来实现,如逐渐加速或逐渐减速。缓动技术可以使界面元素的运动更加平滑和自然,增强用户体验。
2.1.4. 插值器 (Interpolator)在两个已知值中间生成一个或多个中间值,通常用于计算动画的 中间帧。
2.1.5. 关键帧 (Keyframe)动画中 定义特定时间点的帧,用于 控制动画的变化。关键帧允许开发者在动画的不同时间点设置特定的属性值,动画引擎一般会在这些关键帧之间插值生成 中间帧,从而创建平滑的动画效果。
2.1.6. 补间动画 (Tweening)指的是 在两个关键帧之间生成中间帧的过程,使得动画从一个状态平滑地过渡到另一个状态。常规玩法:定义起始状态和结束状态,使用插值器控制动画的变化速率,从而实现各种复杂的动画效果。
2.1.7. 物理动画 (Physics-based Animation)指的是 基于物理的动画,模拟物理现象(如重力、弹性、摩擦、碰撞等)的动画效果。
2.1.8. 附:游戏相关名词精灵 (Sprite):指游戏中的一个二维图像或动画,通常用于表示角色、道具等。时间轴 (Timeline):指动画的时间进程,用于控制动画的播放顺序和时间。状态机 (State Machine):指通过状态和状态转换来控制角色或对象行为的逻辑结构。碰撞检测 (Collision Detection):用于检测游戏对象之间是否发生碰撞。纹理 (Texture):应用于3D模型或2D图形表面的图像。网格 (Mesh):3D模型的几何形状,由顶点和边组成。骨骼动画 (Skeletal Animation):使用骨骼和关节来控制3D模型的动画。粒子系统 (Particle System):用于模拟诸如火焰、烟雾、爆炸等效果的系统。渲染 (Rendering):将3D场景转换为2D图像的过程。光照 (Lighting):用于模拟光源对场景的影响。阴影 (Shadow):由光源和物体遮挡产生的阴影效果。物理引擎 (Physics Engine):用于模拟物理现象,如重力、摩擦、碰撞等。路径规划 (Pathfinding):用于计算角色或对象从一个点移动到另一个点的路径。人工智能 (Artificial Intelligence, AI):用于控制非玩家角色(NPC)的行为和决策。3. Flutter 动画核心 API?? Tips:知道下类名,是干嘛的就行,属性和方法列出来,只是方便用时检索~
3.1. Animation抽象类,主要用于 保存动画的值和状态,还提供了变化监听,常用属性:
status → AnimationStatus,返回当前动画的状态。value → T,返回当前动画的值。isDismissed → bool,检查动画是否在起点停止。isCompleted → bool,检查动画是否在终点停止。常用方法:
addListener (VoidCallback listener):添加值变化的监听器。removeListener (VoidCallback listener):移除值变化的监听器。addStatusListener (AnimationStatusListener listener):添加状态变化的监听器。removeStatusListener (AnimationStatusListener listener):移除状态变化的监听器。drive(Animatable child):将一个 Tween 或 CurveTween 链接到动画上。3.2. Curve-动画曲线抽象类,继承自 ParametricCurve ,用于定义 参数化动画缓动曲线 (动画随时间的变化速率) 。我们把 匀速动画 称为 线性 的,非匀速动画 称为 非线性 的。一般很少 自定义Curve (重写transform方法,实现自定义插值逻辑),而是使用Flutter 给我们提供的 预定义曲线,常用曲线 (通过 Curves 类访问,如Curves.linear ):
缓动曲线名称描述linear线性曲线,匀速变化decelerate开始较快,然后减速,倒置的 f(t) = t2 抛物线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从小变大的动画吧:
classAnimatedBoxextendsStatefulWidget{
constAnimatedBox({super.key});
@override
StatecreateState()=_AnimatedBoxState();
}
//??混入SingleTickerProviderStateMixin以获取vsync
class_AnimatedBoxStateextendsStateAnimatedBoxwithSingleTickerProviderStateMixin{
lateAnimationController_controller;
lateAnimationdouble_animation;
@override
voidinitState(){
super.initState();
//??初始化AnimationController,设置动画时长为2秒
_controller=AnimationController(
duration:constDuration(seconds:2),
vsync:this,
//??初始化Tween补间动画,设置起始值为0,结束值为300,添加值变化监听,setState()刷新UI
_animation=Tween(begin:0.0,end:200.0).animate(_controller)..addListener((){
setState(()
//启动动画(正向执行)
_controller.forward();
}
@override
voiddispose(){
//??释放AnimationController
_controller.dispose();
super.dispose();
}
@override
Widgetbuild(BuildContextcontext){
//??通过Animation.value获取当前动画值,设置宽高
returnContainer(
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();
}elseif(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() 实现一个 阻尼效果 吧:
classDampingCurveextendsCurve{
@override
doubletransform(doublet){
//??实现阻尼效果的曲线逻辑
return(1-(1-t)*(1-
}
}
//调用处:
_animation=Tween(begin:0.0,end:200.0).animate(
CurvedAnimation(parent:_controller,curve:DampingCurve())
);
运行效果如下:
4.5. 多种动画并行?? 单纯的变大变小还不够,再加个旋转试试?
class_AnimatedBoxStateextendsStateAnimatedBoxwithSingleTickerProviderStateMixin{
lateAnimationController_controller;
lateAnimationdouble_sizeAnimation;
lateAnimationdouble_rotationAnimation;
@override
voidinitState(){
super.initState();
//??初始化AnimationController,设置动画时长为2秒
_controller=AnimationController(
duration:constDuration(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
voiddispose(){
//??释放AnimationController
_controller.dispose();
super.dispose();
}
@override
Widgetbuild(BuildContextcontext){
returnTransform.rotate(
angle:_rotationAnimation.value,
child:Container(
width:_sizeAnimation.value,
height:_sizeAnimation.value,
color:Colors.blue,
),
}
}
运行效果如下:
?? 有点意思,要不再加个 从底下往上抛的动画?在添加之前,问读者一个问题:
觉不觉得每新增一个动画,都要写一遍 addListener(() { setState(() {}); }) 有些麻烦?
?? 这个,可以用 Flutter 提供的动画便捷构建工具 → AnimatedBuilder 来解决,它允许我们将动画与UI逻辑分离,从而简化代码并提高可读性。加上上抛动画的代码如下:
//??分别定义缩放、旋转、上抛动画
lateAnimationdouble_sizeAnimation;
lateAnimationdouble_rotationAnimation;
lateAnimationOffset_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=TweenOffset(begin:constOffset(0,300),end:constOffset(0,-200))
.animate(CurvedAnimation(parent:_controller,curve:Curves.easeInOut));
//??AnimatedBuilder只会再动画更新是重建其子树
@override
Widgetbuild(BuildContextcontext){
returnAnimatedBuilder(animation:_controller,builder:(context,child){
returnTransform.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:constInterval(0.0,1.0,curve:DampingCurve()),
));
//??旋转动画在前半段时间运行,即0-1s
_rotationAnimation=Tween(begin:0.0,end:2*pi).animate(CurvedAnimation(
parent:_controller,
curve:constInterval(0.0,0.5,curve:Curves.linear),
));
//??位移动画在前半段时间运行,即1-2s
_parabolicAnimation=
TweenOffset(begin:constOffset(0,200),end:constOffset(0,-200)).animate(CurvedAnimation(
parent:_controller,
curve:constInterval(0.5,1.0,curve:Curves.easeInOut),
));
运行效果如下:
???♀? 当然,如果不想用 Interval 和 Tween 来精确控制每个动画的时间段,也可以为每个动画单独定义一个 AnimationController 进行管理。AnimatedBuilder 的 animation 这样传下动画控制器实例就好:
AnimatedBuilder(animation:Listenable.merge([_sizeController,_rotationController,_parabolicController])
不过,当其中 任意一个AnimationController发生变化,AnimatedBuilder 都会重新构建其子树,在某些场景,为了避免不必要的刷新,可以将不同的动画分离到不同的 AnimatedBuilder 中。
5. 其它 API5.1. TickerFlutter 中用于 驱动动画 的一个类,它会在每一帧调用一个回调函数,从而实现 动画的逐帧更新。创建 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,
//矩形
Rect.lerp(a,b,
//大小
Size.lerp(a,b,
//偏移
Offset.lerp(a,b,
//对齐方式
Alignment.lerp(a,b,
//文本样式
TextStyle.lerp(a,b,
//边框半径
BorderRadius.lerp(a,b,
//a起始边框,b为终止边框
Border.lerp(a,b,
//盒子约束(最大最小宽高)
BoxConstraints.lerp(a,b,
//边距
EdgeInsets.lerp(a,b,
//矩阵
Matrix4.lerp(a,b,
//半径
Radius.lerp(a,b,
//形状边框
ShapeBorder.lerp(a,b,
计算公式一般遵循:返回值 = a + (b - a) * t 。写个简单的颜色插值的简单示例:
classColorLerpExampleextendsStatefulWidget{
@override
_ColorLerpExampleStatecreateState()=_ColorLerpExampleState();
}
class_ColorLerpExampleStateextendsStateColorLerpExamplewithSingleTickerProviderStateMixin{
lateAnimationController_controller;
@override
voidinitState(){
super.initState();
_controller=AnimationController(
duration:constDuration(seconds:2),
vsync:this,
)..repeat(reverse:true);
}
@override
voiddispose(){
_controller.dispose();
super.dispose();
}
@override
Widgetbuild(BuildContextcontext){
returnAnimatedBuilder(
animation:_controller,
builder:(context,child){
//??使用Color.lerp()方法在红色和蓝色之间进行插值
Colorcolor=Color.lerp(Colors.red,Colors.blue,_controller.value)!;
returnContainer(
color:color,
width:100,
height:100,
},
}
}
voidmain(){
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,那 平移 呢?没有,???♀? 那就自己写一个了吧:
classTranslateTransitionextendsAnimatedWidget{
//??传入一个AnimationOffset来控制平移效果
constTranslateTransition({
super.key,
requiredthis.offset,
this.child,
}):super(listenable:offset);
//??控制平移偏移量的动画
finalAnimationOffsetoffset;
finalWidget?child;
@override
Widgetbuild(BuildContextcontext){
//??将偏移动画的值应用到子组件上
returnTransform.translate(
offset:offset.value,
child:child,
}
}
调用处代码:
class_AnimatedBoxStateextendsStateAnimatedBoxwithSingleTickerProviderStateMixin{
lateAnimationController_controller;
lateAnimationdouble_animation;
@override
voidinitState(){
super.initState();
_controller=AnimationController(
duration:constDuration(seconds:2),
vsync:this,
)..repeat(reverse:true);
_animation=_controller;//默认动画曲线是线性的,有需要可以创建CurvedAnimation使用不同的动画曲线
}
@override
voiddispose(){
_controller.dispose();
super.dispose();
}
@override
Widgetbuild(BuildContextcontext){
returnTranslateTransition(
//??创建一个Tween插值器,指定偏移动画的开始值和结束值,调用animate()转换为Animation对象
offset:TweenOffset(
begin:constOffset(0,300),
end:constOffset(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,平移 得自己写???♀?:
classAnimatedTranslateextendsImplicitlyAnimatedWidget{
constAnimatedTranslate({
super.key,
requiredthis.offset,
requiredsuper.duration,
this.child,
finalOffsetoffset;
finalWidget?child;
@override
AnimatedWidgetBaseStatecreateState()=_AnimatedTranslateState();
}
class_AnimatedTranslateStateextendsAnimatedWidgetBaseStateAnimatedTranslate{
//??用于平移的补间动画
TweenOffset?_offsetTween;
//??重写此方法,遍历当前补间动画,检查是否需要创建新的补间动画或更新现有的补间动画,返回新的补间动画
@override
voidforEachTween(TweenVisitordynamicvisitor){
_offsetTween=visitor(
_offsetTween,
widget.offset,
(dynamicvalue)=TweenOffset(begin:valueasOffset),
)asTweenOffset
}
@override
Widgetbuild(BuildContextcontext){
//??根据Animationdouble对象的当前值,计算出对应的插值结果
returnTransform.translate(
offset:_offsetTween?.evaluate(animation)??Offset.zero,
child:widget.child,
}
}
然后 隐式动画 无法主动控制动画的开始和暂停,必须重建组件才能执行动画,是的,需要手动触发 setState() ?? 这里加个按钮点击触发动画值的变化,具体代码如下:
class_AnimatedBoxStateextendsStateAnimatedBoxwithSingleTickerProviderStateMixin{
finalDuration_animationDuration=constDuration(seconds:2);//动画的持续时间
double_animationValue=0.0;//动画的起始值
Offset_offset=constOffset(0,300);//平移的偏移量
@override
Widgetbuild(BuildContextcontext){
returnColumn(
children:[
ElevatedButton(
onPressed:(){
setState((){
//??点击按钮时,切换动画的起始值和平移的偏移量
_animationValue=_animationValue==0.0?1.0:0.0;
_offset=_offset==constOffset(0,300)?constOffset(0,0):constOffset(0,300);
},
child:constText('开始动画'),
),
constSizedBox(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会自动生成过渡帧,使用代码示例如下:
classFirstPageextendsStatelessWidget{
constFirstPage({super.key});
@override
Widgetbuild(BuildContextcontext){
returnScaffold(
appBar:AppBar(title:constText('FirstPage')),
body:Center(
child:GestureDetector(
onTap:(){
Navigator.push(
context,
MaterialPageRoute(builder:(context)=constSecondPage()),
},
child:Hero(
tag:'hero-tag',
child:Align(
alignment:Alignment.topLeft,
child:Container(
width:100,
height:100,
color:Colors.blue,
),
),
),
),
),
}
}
classSecondPageextendsStatelessWidget{
constSecondPage({super.key});
@override
Widgetbuild(BuildContextcontext){
returnScaffold(
appBar:AppBar(title:constText('SecondPage')),
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';
voidmain(){
runApp(constMaterialApp(home:PhysicsCardDragDemo()));
}
classPhysicsCardDragDemoextendsStatelessWidget{
constPhysicsCardDragDemo({super.key});
@override
Widgetbuild(BuildContextcontext){
returnScaffold(
appBar:AppBar(),
body:constDraggableCard(
child:FlutterLogo(
size:64,
),
),
}
}
//一个支持拖拽,且松手后会回到中心位置的卡片
classDraggableCardextendsStatefulWidget{
constDraggableCard({requiredthis.child,super.key});
finalWidgetchild;
@override
StateDraggableCardcreateState()=_DraggableCardState();
}
class_DraggableCardStateextendsStateDraggableCardwithSingleTickerProviderStateMixin{
lateAnimationController_controller;
Alignment_dragAlignment=Alignment.center;//卡片的对齐方式
lateAnimationAlignment_animation;//动画
void_runAnimation(OffsetpixelsPerSecond,Sizesize){
//创建一个动画,从当前位置移动到中心位置
_animation=_controller.drive(
AlignmentTween(
begin:_dragAlignment,
end:Alignment.center,
),
//计算每秒的单位移动量
finalunitsPerSecondX=pixelsPerSecond.dx/size.width;
finalunitsPerSecondY=pixelsPerSecond.dy/size.height;
finalunitsPerSecond=Offset(unitsPerSecondX,unitsPerSecondY);
finalunitVelocity=unitsPerSecond.distance;
//定义弹簧动画的参数
constspring=SpringDescription(
mass:30,
stiffness:1,
damping:1,
//创建弹簧动画模拟
finalsimulation=SpringSimulation(spring,0,1,-unitVelocity);
//使用弹簧动画来驱动控制器
_controller.animateWith(simulation);
}
@override
voidinitState(){
super.initState();
_controller=AnimationController(vsync:this);
//添加监听器,当动画值发生变化时,更新_dragAlignment的值,刷新UI
_controller.addListener((){
setState((){
_dragAlignment=_animation.value;
}
@override
voiddispose(){
_controller.dispose();
super.dispose();
}
@override
Widgetbuild(BuildContextcontext){
finalsize=MediaQuery.of(context).size;
returnGestureDetector(
//当用户按下时,停止动画
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 中的动画详解》
点击公众关注号,“技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线