Flutter动画开发全解析
1. 引言
在Flutter开发中,动画是提升用户体验的重要手段。Flutter框架提供了丰富的动画支持,内置了多种可直接使用的动画,同时也允许开发者通过组合和扩展这些动画来满足不同的需求。在深入学习动画之前,我们需要了解一些重要的概念和相关类。
2. 动画基础概念
2.1 Animation 类
在Flutter中,动画由状态(status)和类型为T的值(value)组成。状态表示动画的运行状态,如正在运行或已完成;值则是动画当前的值,并且在动画执行过程中会发生变化。Animation 类不仅保存这些信息,还提供了回调函数,让其他类可以了解动画的运行状态和当前值。
需要注意的是,Animation 类实例仅负责保存和暴露这些值,它不涉及视觉反馈、屏幕绘制或绘制方式(即build()函数)。常见的动画类型是Animation ,因为双精度值可以方便地在比例空间中操作各种值。
Animation类会在指定的最小值和最大值之间生成一个值序列(不一定是线性的),这个过程称为插值(interpolation)。插值可以是线性的,也可以定义为阶跃函数或曲线。Flutter提供了多个用于操作动画的函数和工具,如下所示:
- AnimationController:尽管名字中有“Controller”,但它并非用于控制动画对象,而是帮助自身进行控制,因为它继承自Animation类,本身也是一个动画。
- CurvedAnimation:将曲线应用于另一个动画的动画。
- Tween:用于在起始值和结束值之间创建线性插值。
Animation类还提供了在运行周期内访问其状态和值的方法。通过状态监听器(status listeners),我们可以知道动画何时开始、结束或反向运行。使用addStatusListener()方法,我们可以根据动画的开始或结束事件来操作我们的小部件。同样,使用addListener()方法添加值监听器,每当动画值发生变化时,我们都会收到通知,并可以使用setState() {}方法重建小部件。
2.2 AnimationController
AnimationController是Flutter中最常用的动画类之一,它派生自Animation
类,并添加了一些用于操作动画的基本方法。Animation类是Flutter动画的基础,但它没有与动画控制相关的方法,而AnimationController则为动画概念添加了这些控制,例如:
- 播放和停止控制:AnimationController可以让动画向前播放、向后播放或停止。
- 持续时间:真实的动画有有限的播放时间,即它们会播放一段时间后结束或重复。
- 设置动画当前值:这会导致动画停止,并通知状态和值监听器。
- 定义动画的上下限:这样我们可以在播放动画前后知道预期的值。
下面是AnimationController的构造函数及其主要属性的分析:
AnimationController({
double value,
Duration duration,
String debugLabel,
double lowerBound: 0.0,
double upperBound: 1.0,
AnimationBehavior animationBehavior: AnimationBehavior.normal,
@required TickerProvider vsync
})
各属性含义如下:
| 属性 | 含义 |
| ---- | ---- |
| value | 动画的初始值,如果未指定则默认为lowerBound。 |
| duration | 动画的持续时间。 |
| debugLabel | 用于调试的字符串,在调试输出中标识控制器。 |
| lowerBound | 动画被视为取消的最小值,通常是运行时的起始值,不能为null。 |
| upperBound | 动画被视为完成的最大值,通常是运行时的结束值,不能为null。 |
| animationBehavior | 配置AnimationController在动画禁用时的行为。如果是AnimationBehavior.normal,动画持续时间将减少;如果是AnimationBehavior.preserve,AnimationController将保留其行为。 |
| vsync | 控制器用于在帧触发时获取信号的TickerProvider实例。 |
你可以查看 AnimationController类的文档 以了解所有可用的运行动画方法。
2.3 TickerProvider和Ticker
TickerProvider接口描述了能够提供Ticker对象的对象。Ticker用于需要知道下一帧何时构建的类,通常通过AnimationControllers间接使用。当使用State类时,我们可以通过扩展TickerProviderStateMixin或SingleTickerProviderStateMixin来拥有TickerProvider,并将其与AnimationController对象一起使用。
2.4 CurvedAnimation
CurvedAnimation类用于将Animation类的进度定义为非线性曲线。我们可以使用它来修改现有动画的插值方法。当我们希望在动画向前播放和反向播放时使用不同的曲线时,它也很有用,分别通过其curve和reverseCurve属性实现。
Curves类定义了许多可直接用于动画的曲线,而不仅仅是Curves.linear。你可以查看 Curves类的文档 以详细了解每条曲线的行为。
2.5 Tween
除了上述类之外,Tween类可以帮助我们处理动画的范围相关的特定任务。默认情况下,动画的简单起始值和结束值分别是0.0和1.0。通过使用Tween,我们可以在不修改AnimationController的情况下更改其范围或类型。Tween可以是任何类型,我们也可以根据需要创建自定义的Tween类。
Tween的作用是在起始值和结束值之间的时间段内返回值,我们可以将这些值作为属性传递给正在动画化的任何内容,从而使其不断更新。例如,我们可以使用特定的Tween来更改小部件的大小、位置、不透明度、颜色等。
此外,还有其他Tween派生类,如CurveTween类可以修改动画曲线,ColorTween类可以在颜色之间创建插值。
3. 使用动画
在处理动画时,我们并不总是创建完全相同的动画对象,但可以发现一些使用场景的相似性。Tween对象对于更改动画的类型和范围很有用。大多数情况下,我们会使用AnimationController、CurvedAnimation和Tween实例来组合动画。
在使用自定义Tween实现之前,让我们通过动画方式重新审视之前的小部件变换,这样可以获得相同的最终效果,但更加平滑和出色。
3.1 旋转动画
我们可以使用AnimationController类使按钮的旋转变得渐进,而不是直接更改按钮的旋转角度。以下是一个示例代码:
_rotationAnimationButton() {
return Transform.rotate(
angle: _angle,
child: RaisedButton(
child: Text("Rotated button"),
onPressed: () {
if (_animation.status == AnimationStatus.completed) {
_animation.reset();
_animation.forward();
}
},
),
);
}
需要注意的是:
- 角度值现在通过_angle属性定义,而不是直接赋值给一个字面量。
- 在onPressed属性中,我们检查_animation是否完成,如果完成,则从开始重复动画。
下面是创建和运行AnimationController对象的示例类:
class _RotationAnimationsState extends State<RotationAnimations> with
SingleTickerProviderStateMixin {
double _angle = 0.0;
AnimationController _animation;
...
}
在这个类中,有几点需要注意:
- 我们有一个名为RotationAnimations的StatefulWidget对象,使用SingleTickerProviderStateMixin类来提供控制器运行所需的Ticker对象。
- 我们有一个_angle属性,用于定义按钮的当前角度。可以使用setState()方法使小部件以新的角度重建。
- 我们有一个_animation对象,用于保存动画并允许我们管理它。
在State类的initState()函数中设置并启动动画是一个很好的选择:
@override
void initState() {
super.initState();
_animation = createRotationAnimation();
_animation.forward();
}
下面是定义动画的方法:
createRotationAnimation() {
var animation = AnimationController(
vsync: this,
debugLabel: "animations demo",
duration: Duration(seconds: 3),
);
animation.addListener(() {
setState(() {
_angle = (animation.value * 360.0) * _toRadians;
});
});
return animation;
}
动画的创建可以分为两个重要部分:
1. 动画定义本身,我们设置了动画的debugLabel属性用于调试,设置了vsync以便它可以有一个Ticker并知道何时生成新的动画值,最后设置了动画的持续时间。
2. 监听动画值的变化。每当动画有新值时,我们将其乘以360度,从而得到一个成比例的旋转值。
如果需要,我们可以使用CurveTween为动画添加不同的曲线,例如在createBounceInRotationAnimation()方法中:
createBounceInRotationAnimation() {
var controller = AnimationController(
vsync: this,
debugLabel: "animations demo",
duration: Duration(seconds: 3),
);
var animation = controller.drive(CurveTween(
curve: Curves.bounceIn,
));
animation.addListener(() {
setState(() {
_angle = (animation.value * 360.0) * _toRadians;
});
});
return controller;
}
这里,我们使用控制器的drive()方法创建另一个Animation实例,并通过CurveTween对象传递所需的曲线。需要注意的是,我们将监听器添加到新的动画对象而不是控制器,因为我们需要相对于曲线的值。
另外,在State类的生命周期结束时,我们必须处理AnimationController类实例,以防止内存泄漏:
@override
void dispose() {
_animation.dispose();
super.dispose();
}
3.2 缩放动画
为了创建缩放动画并获得比直接更改缩放属性更好的效果,我们可以再次使用AnimationController类。以下是构建带有缩放效果的RaisedButton小部件的代码:
_scaleAnimationButton() {
return Transform.scale(
scale: _scale,
child: RaisedButton(
child: Text("Scaled button"),
onPressed: () {
if (_animation.status == AnimationStatus.completed) {
_animation.reverse();
} else if (_animation.status == AnimationStatus.dismissed) {
_animation.forward();
}
},
),
);
}
在这个示例中,我们使用了_scale属性,并在onPressed方法中进行了更改。如果动画完成,我们使用AnimationController的reverse()函数反向播放动画;如果动画处于初始状态,则向前播放。
动画对象的创建与旋转动画类似,但在控制器的构造上有一些细微的修改:
createScaleAnimation() {
var animation = AnimationController(
vsync: this,
lowerBound: 1.0,
upperBound: 2.0,
debugLabel: "animations demo",
duration: Duration(seconds: 2),
);
animation.addListener(() {
setState(() {
_scale = animation.value;
});
});
return animation;
}
这里,我们更改了控制器的lowerBound和upperBound值,以使按钮增长到两倍大小,并且不希望它小于初始大小(scale = 1.0)。此外,我们更改了动画值监听器,直接从动画中获取值,无需进行任何计算。
3.3 平移动画
同样,我们可以使用AnimationController使平移变换看起来更好、更平滑。以下是创建平移动画的代码:
createTranslateAnimation() {
var controller = AnimationController(
vsync: this,
debugLabel: "animations demo",
duration: Duration(seconds: 2),
);
var animation = controller.drive(Tween<Offset>(
begin: Offset.zero,
end: Offset(70, 200),
));
animation.addListener(() {
setState(() {
_offset = animation.value;
});
});
return controller;
}
在这个示例中,我们使用了Tween 实例,并通过drive()方法将其传递给AnimationController对象,就像之前使用CurveTween一样。这是因为Offset类重写了减法和加法等数学运算符,使得计算中间偏移量(动画值)成为可能,从而实现两个Offset值之间的插值。
你可以查看 Offset类的源代码 以了解更多细节。需要注意的是,要创建自定义插值,我们通常需要编写自定义的Tween类。
4. 多种变换和自定义Tween
我们可以使用Matrix4类组合多种变换,对于动画也是类似的,我们可以组合动画、依次运行它们并播放它们。为了创建组合动画,我们可以基于单个Animation对象创建多个变换值。
以下是实现组合动画的步骤:
1. 在类中定义多个值:
class _ComposedAnimationsState extends State<ComposedAnimations>
with SingleTickerProviderStateMixin {
Offset _offset = Offset.zero;
double _scale = 1.0;
double _angle = 0.0;
...
}
- 每当动画值发生变化时,根据它计算我们的值:
animation.addListener(() {
setState(() {
_offset = Offset(animation.value * 70, animation.value * 200);
_scale = 1.0 + animation.value;
_angle = 360 * animation.value;
});
});
- 在build()方法中应用我们在动画执行的每个步骤中计算的值:
_composedAnimationButton() {
return Transform.translate(
offset: _offset,
child: Transform.rotate(
angle: _angle * _toRadians,
child: Transform.scale(
scale: _scale,
child: RaisedButton(
child: Text("multiple animation"),
onPressed: () {
if (_animation.status == AnimationStatus.completed) {
_animation.reverse();
} else if (_animation.status == AnimationStatus.dismissed) {
_animation.forward();
}
},
),
),
),
);
}
对于简单的情况,这样做是最好的,因为我们需要处理的对象更少,并且只播放一个动画。然而,为了使代码更易于维护,最好将值的计算与动画本身分离,这就是我们可以使用Tween的地方。
5. 自定义Tween
要创建自定义Tween类,首先需要定义我们的值对象。这里,我们选择将变换值分组:
class ButtonTransformation {
final double scale;
final double angle;
final Offset offset;
// this none getter returns a initial state of transformation
// with default scale, no rotation or translation
static ButtonTransformation get none => ButtonTransformation(
scale: 1.0,
angle: 0.0,
offset: Offset.zero,
);
}
然后,我们使用定义的类型扩展Tween类:
class CustomTween extends Tween<ButtonTransformation> {
CustomTween({ButtonTransformation begin, ButtonTransformation end} ):
super(begin: begin, end: end,);
@override
lerp(double t) {
return super.lerp(t);
}
}
我们需要定义自定义Tween的lerp()方法(lerp代表线性插值),该方法负责根据t值返回begin和end之间的中间ButtonTransformation值。
通过查看默认Tween类的lerp()实现,我们可以看到它非常简单:
// part of tween.dart Tween class
@protected
T lerp(double t) {
assert(begin != null);
assert(end != null);
return begin + (end - begin) * t;
}
它使用类型T对象上的 +、 - 和 * 运算符来计算lerp()值。这意味着我们可以在ButtonTransformation类中实现这些运算符,这样Tween就可以像处理其他类型一样工作:
class ButtonTransformation {
...
ButtonTransformation operator -(ButtonTransformation other) =>
ButtonTransformation(
scale: scale - other.scale,
angle: angle - other.angle,
offset: offset - other.offset,
);
ButtonTransformation operator +(ButtonTransformation other) =>
ButtonTransformation(
scale: scale + other.scale,
angle: angle + other.angle,
offset: offset + other.offset,
);
ButtonTransformation operator *(double t) => ButtonTransformation(
scale: scale * t,
angle: angle * t,
offset: offset * t,
);
}
现在,Tween类能够生成中间的ButtonTransformation值。我们可以像之前一样使用生成的动画值:
createCustomTweenAnimation() {
var controller = AnimationController(
vsync: this,
debugLabel: "animations demo",
duration: Duration(seconds: 3),
);
var animation = controller.drive(CustomTween(
begin: ButtonTransformation.none, // initial state of the animation
end: ButtonTransformation(
angle: 360.0,
offset: Offset(70, 200),
scale: 2.0,
)));
animation.addListener(() {
setState(() {
_buttonTransformation = animation.value;
});
});
return controller;
}
这里的主要区别在于使用了CustomTween属性。需要注意的是,我们始终需要定义begin和end值,因为Tween是基于相应插值定义的范围。
通过这些示例,我们了解了如何在Flutter中使用和应用最重要的动画。我们还可以使用单独的Animation对象构建多个同时进行的动画,通常通过将相同的AnimationController设置为它们的父对象。由于我们将使用相同的Ticker对象,因此它们可以保证同步。
6. 替代动画应用方式
除了前面介绍的使用AnimationController、Tween等组合动画的方式,Flutter还提供了其他一些替代方式来应用动画到我们的小部件上。
6.1 AnimatedBuilder
AnimatedBuilder是一个非常有用的小部件,它可以帮助我们将动画逻辑和小部件的构建逻辑分离。使用AnimatedBuilder,我们可以将动画值传递给一个builder函数,在这个函数中构建需要动画化的小部件。
下面是一个使用AnimatedBuilder实现旋转动画的示例:
class RotationAnimationWithBuilder extends StatefulWidget {
@override
_RotationAnimationWithBuilderState createState() => _RotationAnimationWithBuilderState();
}
class _RotationAnimationWithBuilderState extends State<RotationAnimationWithBuilder>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 3),
);
_animation = Tween<double>(begin: 0.0, end: 2 * math.pi).animate(_controller);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (BuildContext context, Widget child) {
return Transform.rotate(
angle: _animation.value,
child: RaisedButton(
child: Text("Rotated with Builder"),
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reset();
_controller.forward();
}
},
),
);
},
);
}
}
在这个示例中,我们创建了一个AnimationController和一个Animation 对象。在build方法中,使用AnimatedBuilder将动画值传递给builder函数,在函数内部根据动画值构建旋转的按钮。这样,动画逻辑和小部件构建逻辑就分离了,代码更加清晰和易于维护。
6.2 AnimatedWidget
AnimatedWidget是一个抽象类,我们可以通过继承它来创建自定义的动画小部件。当动画值发生变化时,AnimatedWidget会自动调用build方法进行重绘。
下面是一个使用AnimatedWidget实现缩放动画的示例:
class ScalingAnimatedWidget extends AnimatedWidget {
ScalingAnimatedWidget({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return Transform.scale(
scale: animation.value,
child: RaisedButton(
child: Text("Scaled with AnimatedWidget"),
onPressed: () {},
),
);
}
}
class ScalingAnimationExample extends StatefulWidget {
@override
_ScalingAnimationExampleState createState() => _ScalingAnimationExampleState();
}
class _ScalingAnimationExampleState extends State<ScalingAnimationExample>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
lowerBound: 1.0,
upperBound: 2.0,
);
_animation = _controller;
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScalingAnimatedWidget(animation: _animation);
}
}
在这个示例中,我们创建了一个ScalingAnimatedWidget类,继承自AnimatedWidget。在build方法中,根据动画值构建缩放的按钮。在ScalingAnimationExample类中,创建AnimationController和Animation对象,并将动画传递给ScalingAnimatedWidget。
7. 动画同步与组合
在实际开发中,我们可能需要同时运行多个动画,并且保证它们之间的同步。通常可以通过将相同的AnimationController作为多个Animation对象的父对象来实现。
下面是一个同时运行旋转和缩放动画的示例:
class CombinedAnimations extends StatefulWidget {
@override
_CombinedAnimationsState createState() => _CombinedAnimationsState();
}
class _CombinedAnimationsState extends State<CombinedAnimations>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _rotationAnimation;
Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 3),
);
_rotationAnimation = Tween<double>(begin: 0.0, end: 2 * math.pi).animate(_controller);
_scaleAnimation = Tween<double>(begin: 1.0, end: 2.0).animate(_controller);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return Transform.rotate(
angle: _rotationAnimation.value,
child: Transform.scale(
scale: _scaleAnimation.value,
child: RaisedButton(
child: Text("Combined Animations"),
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reset();
_controller.forward();
}
},
),
),
);
},
);
}
}
在这个示例中,我们创建了一个AnimationController,并使用它来控制两个动画:旋转动画和缩放动画。在build方法中,使用AnimatedBuilder根据动画值构建同时具有旋转和缩放效果的按钮。由于两个动画都使用同一个AnimationController,它们会保持同步。
8. 总结
通过本文的介绍,我们全面了解了Flutter中动画开发的各种方法和技巧。从动画的基础概念,如Animation 类、AnimationController、TickerProvider等,到具体的动画应用,如旋转动画、缩放动画、平移动画等,再到多种变换的组合和自定义Tween的创建,以及替代的动画应用方式AnimatedBuilder和AnimatedWidget,我们掌握了丰富的动画开发知识。
在实际开发中,我们可以根据具体的需求选择合适的动画实现方式。对于简单的动画,直接使用AnimationController和Tween组合可能就足够了;对于复杂的动画场景,使用AnimatedBuilder和AnimatedWidget可以使代码更加清晰和易于维护。同时,通过将多个动画与同一个AnimationController关联,可以实现动画的同步。
希望这些知识能够帮助你在Flutter开发中创建出更加生动和吸引人的用户界面。
下面是一个简单的动画开发流程总结:
1. 确定动画需求,如旋转、缩放、平移等。
2. 创建AnimationController对象,设置持续时间等属性。
3. 根据需求创建相应的Animation对象,如Tween、CurvedAnimation等。
4. 根据动画类型选择合适的实现方式:
- 简单动画:直接使用AnimationController和Tween组合。
- 分离动画逻辑和构建逻辑:使用AnimatedBuilder。
- 创建自定义动画小部件:继承AnimatedWidget。
5. 在build方法中根据动画值构建小部件。
6. 在dispose方法中释放AnimationController资源,防止内存泄漏。
mermaid流程图如下:
graph TD
A[确定动画需求] --> B[创建AnimationController]
B --> C[创建Animation对象]
C --> D{选择实现方式}
D -->|简单动画| E[AnimationController + Tween]
D -->|分离逻辑| F[AnimatedBuilder]
D -->|自定义小部件| G[AnimatedWidget]
E --> H[构建小部件]
F --> H
G --> H
H --> I[释放资源]
通过遵循这个流程,你可以更加高效地进行Flutter动画开发。
超级会员免费看
1294

被折叠的 条评论
为什么被折叠?



