参考资料:《Flutter实战·第二版》9.7 动画过渡组件
"动画过渡组件"指的是在Widget属性发生变化时会执行过渡动画的组件,其最明显的一个特征就是会在内部管理一个AnimationController
。controller
定义了过渡动画的时长,而animation
对象的定义过程中会指明动画的曲线、添加监听,通过Tween
对象指明动画的区间起止值。
9.7.1 自定义动画过渡组件
要实现一个AnimatedDecoratedBox
,它可以在decoration
属性发生变化时,从旧状态变成新状态的过程可以执行一个过渡动画。想要实现一个外观改变过渡的组件,首先需要定义一个Stateful Widget,并提供需要输入的参数,包含、子Widget、曲线样式等,完整的实现代码如下:
dart
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
home: const MyHomePage(title: 'TEAL WORLD'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(
widget.title,
style: TextStyle(
color: Colors.teal.shade800, fontWeight: FontWeight.w900),
),
actions: [
ElevatedButton(
child: const Icon(Icons.refresh),
onPressed: () {
setState(() {});
},
)
],
),
body: const AnimatedTestRoute(),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: Icon(
Icons.add_box,
size: 30,
color: Colors.teal[400],
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class AnimatedTestRoute extends StatefulWidget {
const AnimatedTestRoute({super.key});
@override
AnimatedTestRouteState createState() => AnimatedTestRouteState();
}
class AnimatedTestRouteState extends State<AnimatedTestRoute> with SingleTickerProviderStateMixin {
Color _decorationColor = Colors.blue;
var duration = const Duration(seconds: 1);
@override
Widget build(BuildContext context) {
return AnimatedDecoratedBox1(
duration: duration,
decoration: BoxDecoration(color: _decorationColor),
child: TextButton(
onPressed: () {
setState(() {
_decorationColor = Colors.red;
});
},
child: const Text(
"AnimatedDecoratedBox",
style: TextStyle(color: Colors.white),
),
),
);
}
}
class AnimatedDecoratedBox1 extends StatefulWidget {
const AnimatedDecoratedBox1({
Key? key,
required this.decoration,
required this.child,
this.curve = Curves.linear,
required this.duration,
this.reverseDuration,
}) : super(key: key);
final BoxDecoration decoration;
final Widget child;
final Duration duration;
final Curve curve;
final Duration? reverseDuration;
@override
AnimatedDecoratedBox1State createState() => AnimatedDecoratedBox1State();
}
class AnimatedDecoratedBox1State extends State<AnimatedDecoratedBox1>
with SingleTickerProviderStateMixin {
@protected
AnimationController get controller => _controller;
late AnimationController _controller;
Animation<double> get animation => _animation;
late Animation<double> _animation;
late DecorationTween _tween;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return DecoratedBox(
decoration: _tween.animate(_animation).value,
child: child,
);
},
child: widget.child,
);
}
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
reverseDuration: widget.reverseDuration,
vsync: this,
);
_tween = DecorationTween(begin: widget.decoration);
_updateCurve();
}
void _updateCurve() {
_animation = CurvedAnimation(parent: _controller, curve: widget.curve);
}
@override
void didUpdateWidget(AnimatedDecoratedBox1 oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.curve != oldWidget.curve) _updateCurve();
_controller.duration = widget.duration;
_controller.reverseDuration = widget.reverseDuration;
//正在执行过渡动画
if (widget.decoration != (_tween.end ?? _tween.begin)) {
_tween
..begin = _tween.evaluate(_animation)
..end = widget.decoration;
_controller
..value = 0.0
..forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Flutter中,动画的产生是伴随Widget重构的,animation的值是一直变化的,但是只有Widget重建时UI界面才会随之变化。
初始化阶段,在使用AnimatedDecoratedBox
组件的位置定义了样式类型和执行时长,传入组件后initState()
方法中定义_controller
和_tween
对象,随后定义_animation
,此时动画还没有执行,且_tween
的结束样式还没有定义。
然后通过build()
方法构建UI界面,传入定义好的_animation
对象并通过_tween.animate()方法取值设置盒子颜色,因为此时动画没执行,所以盒子是静止蓝色的。child元素是一个按钮,其按下时能够改变传入参数_decorationColor
,使其变为红色。didUpdateWidget()
方法在build()
结束后会被调用进行一次重新构建(似乎是由鼠标hover引起的,暂时原因不明),但当前的参数并没有变化,而且widget.decoration
的值还是蓝色,因此界面没有任何变化。通过在控制台打印信息可以印证该结论:
按钮还没有按下时(此时_tween.end==null
,_tween.begin
为初始值),条件if (widget.decoration != (_tween.end ?? _tween.begin))
不成立,不进行动画触发;
按钮按下时,由于AnimatedDecoratedBox
的外部参数发生了变化,触发了didUpdateWidget()
方法,此时上面的条件就成立了,也因此设置好了起止状态并开始了插值动画。动画进行时,子Widget(按钮)会不断的重新build,从控制台打印信息可以观察这一现象:
bash
AnimatedDecoratedBox initState - decoration = BoxDecoration(color: MaterialColor(primary value: Color(0xff2196f3)))
AnimatedDecoratedBox build
Child build! - animated color = BoxDecoration(color: MaterialColor(primary value: Color(0xff2196f3))) - decoration color = BoxDecoration(color: MaterialColor(primary value: Color(0xff2196f3))) - begin=BoxDecoration(color: MaterialColor(primary value: Color(0xff2196f3))) - end=null
didUpdateWidget
AnimatedDecoratedBox build
Child build! - animated color = BoxDecoration(color: MaterialColor(primary value: Color(0xff2196f3))) - decoration color = BoxDecoration(color: MaterialColor(primary value: Color(0xff2196f3))) - begin=BoxDecoration(color: MaterialColor(primary value: Color(0xff2196f3))) - end=null
didUpdateWidget
AnimatedDecoratedBox build
Child build! - animated color = BoxDecoration(color: MaterialColor(primary value: Color(0xff2196f3))) - decoration color = BoxDecoration(color: MaterialColor(primary value: Color(0xfff44336))) - begin=BoxDecoration(color: MaterialColor(primary value: Color(0xff2196f3))) - end=BoxDecoration(color: MaterialColor(primary value: Color(0xfff44336)))
Child build! - animated color = BoxDecoration(color: Color(0xff2295f1)) - decoration color = BoxDecoration(color: MaterialColor(primary value: Color(0xfff44336))) - begin=BoxDecoration(color: MaterialColor(primary value: Color(0xff2196f3))) - end=BoxDecoration(color: MaterialColor(primary value: Color(0xfff44336)))
...
Child build! - animated color = BoxDecoration(color: MaterialColor(primary value: Color(0xfff44336))) - decoration color = BoxDecoration(color: MaterialColor(primary value: Color(0xfff44336))) - begin=BoxDecoration(color: MaterialColor(primary value: Color(0xff2196f3))) - end=BoxDecoration(color: MaterialColor(primary value: Color(0xfff44336)))
应该注意的是,一般定义动画组件对象的方法都是内部定义controller和animation对象,只应用控制动画播放、反向播放等逻辑即可,其起止状态已知 ;但起止状态需要动作触发、且需要外部参数的组件,需要获取到终止状态才能进行动画。
上面例子的最终效果如图所示:
但上面的代码中,_controller
的管理以及_tween
的更新代码是可以通用的,Flutter中提供了已经封装好的基类ImplicitlyAnimatedWidget
可以帮我们轻松地实现上述功能。AnimationController
的管理就在ImplicitlyAnimatedWidgetState
类中。如果要封装动画,要分别继承ImplicitlyAnimatedWidget
和ImplicitlyAnimatedWidgetState
类。下面将介绍如何使用这两个类来实现同样的变色按钮。
首先继承ImplicitlyAnimatedWidget
类,构造函数需要定义好要传入的参数和子Widget,父类中已经定义好了curve
、duration
、reverseDuration
这三个属性,其中duration
为必传参数,curve
的初始值为线性渐变:
dart
class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
const AnimatedDecoratedBox({
Key? key,
required this.decoration,
required this.child,
Curve curve = Curves.linear,
required Duration duration,
}) : super(
key: key,
curve: curve,
duration: duration,
);
final BoxDecoration decoration;
final Widget child;
@override
_AnimatedDecoratedBoxState createState() {
return _AnimatedDecoratedBoxState();
}
}
其次,状态对象继承AnimatedWidgetBaseState
(该类继承自ImplicitlyAnimatedWidgetState
类)类。其中实现了build()
和forEachTween()
两个方法,build()
中直接通过animation的值构建每个动画帧内的子Widget,而forEachTween()
则用来定义Tween对象的起止值。终止值是通过参数获取的,但初始值是有两种情况的:
- AnimatedDecoratedBox首次build,此时直接将其decoration值置为起始状态,和上面的描述相同 。
- AnimatedDecoratedBox的decoration更新时,则起始状态需要设置成当前的
_tween.evaluate(_animation)
值,显然这可能是一个在变化中的值。这个很难想,什么时候decoration
会更新。首先只有当didUpdateWidget()
触发的时候才会进行判断,也就是此刻外部传入的decoration
发生了变化。无论在动画结束还是动画进行中,当终止状态发生改变时,要重新设置起点并开始动画。
继承AnimatedWidgetBaseState
的代码如下:
dart
class _AnimatedDecoratedBoxState
extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
late DecorationTween _decoration = DecorationTween(begin: widget.decoration);
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: _decoration.evaluate(animation),
child: widget.child,
);
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_decoration = visitor(
_decoration,
widget.decoration,
(value) => DecorationTween(begin: value),
) as DecorationTween;
}
}
初始化时,通过(value) => DecorationTween(begin: value)
构建Tween对象,只有起始值。而中间如果_decoration
有变化,则更新起始值。其中visitor
的定义如下:
dart
Tween<T> visitor(
Tween<T> tween, //当前的tween,第一次调用为null
T targetValue, // 终止状态
TweenConstructor<T> constructor,//Tween构造器,在上述三种情况下会被调用以更新tween
);
实现效果依然和之前一样:
为了验证上面的第二点,可以做一个小实验,可以在调用 AnimatedDecoratedBox
组件的地方再设计一个按钮,用于改变当前decoration
参数的值,假设这里点击后decoration
会变为绿色。当按钮颜色变化中或者变化结束时可以点击按钮观察颜色,这样就能判断"不同初始值"的含义了:
dart
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
home: const MyHomePage(title: 'TEAL WORLD'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(
widget.title,
style: TextStyle(
color: Colors.teal.shade800, fontWeight: FontWeight.w900),
),
actions: [
ElevatedButton(
child: const Icon(Icons.refresh),
onPressed: () {
setState(() {});
},
)
],
),
body: const AnimatedTestRoute(),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: Icon(
Icons.add_box,
size: 30,
color: Colors.teal[400],
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class AnimatedTestRoute extends StatefulWidget {
const AnimatedTestRoute({super.key});
@override
AnimatedTestRouteState createState() => AnimatedTestRouteState();
}
class AnimatedTestRouteState extends State<AnimatedTestRoute> with SingleTickerProviderStateMixin {
Color _decorationColor = Colors.blue;
var duration = const Duration(seconds: 2);
@override
Widget build(BuildContext context) {
return Row(children: [
Expanded(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedDecoratedBox(
duration: duration,
decoration: BoxDecoration(color: _decorationColor),
child: TextButton(
onPressed: () {
setState(() {
_decorationColor = Colors.red;
});
},
child: const Text(
"AnimatedDecoratedBox",
style: TextStyle(color: Colors.white),
),
),
),
const SizedBox(height: 20.0,),
TextButton(onPressed: (){setState(() {
_decorationColor = Colors.green;
});}, child: const Text("Change Color!"))
],
))
],);
}
}
class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
const AnimatedDecoratedBox({
Key? key,
required this.decoration,
required this.child,
Curve curve = Curves.linear,
required Duration duration,
}) : super(
key: key,
curve: curve,
duration: duration,
);
final BoxDecoration decoration;
final Widget child;
@override
AnimatedDecoratedBoxState createState() {
return AnimatedDecoratedBoxState();
}
}
class AnimatedDecoratedBoxState
extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
late DecorationTween _decoration = DecorationTween(begin: widget.decoration);
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: _decoration.evaluate(animation),
child: widget.child,
);
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_decoration = visitor(
_decoration,
widget.decoration,
(value) => DecorationTween(begin: value),
) as DecorationTween;
}
}
为了便于观察,我们可以把duration
设置为2s,此时,如果在按钮完全变成红色后,点击下面的按钮,颜色就会从红色变成绿色:
如果在变色过程中点击下面的按钮,就会从蓝红中间的某个过渡色再花上2s变成绿色:
9.7.2 Flutter预置的动画过渡组件
Flutter SDK中也预置了很多动画过渡组件,实现方式和大都和AnimatedDecoratedBox
差不多:
组件名 | 功能 |
---|---|
AnimatedPadding | 在padding发生变化时会执行过渡动画到新状态 |
AnimatedPositioned | 配合Stack一起使用,当定位状态发生变化时会执行过渡动画到新的状态。 |
AnimatedOpacity | 在透明度opacity发生变化时执行过渡动画到新状态 |
AnimatedAlign | 当alignment发生变化时会执行过渡动画到新的状态。 |
AnimatedContainer | 当Container属性发生变化时会执行过渡动画到新的状态。 |
AnimatedDefaultTextStyle | 当字体样式发生变化时,子组件中继承了该样式的文本组件会动态过 |
可以通过下面的示例代码来查看具体运行效果:
dart
import 'package:flutter/material.dart';
class AnimatedWidgetsTest extends StatefulWidget {
const AnimatedWidgetsTest({Key? key}) : super(key: key);
@override
_AnimatedWidgetsTestState createState() => _AnimatedWidgetsTestState();
}
class _AnimatedWidgetsTestState extends State<AnimatedWidgetsTest> {
double _padding = 10;
var _align = Alignment.topRight;
double _height = 100;
double _left = 0;
Color _color = Colors.red;
TextStyle _style = const TextStyle(color: Colors.black);
Color _decorationColor = Colors.blue;
double _opacity = 1;
@override
Widget build(BuildContext context) {
var duration = const Duration(milliseconds: 400);
return SingleChildScrollView(
child: Column(
children: <Widget>[
ElevatedButton(
onPressed: () {
setState(() {
_padding = 20;
});
},
child: AnimatedPadding(
duration: duration,
padding: EdgeInsets.all(_padding),
child: const Text("AnimatedPadding"),
),
),
SizedBox(
height: 50,
child: Stack(
children: <Widget>[
AnimatedPositioned(
duration: duration,
left: _left,
child: ElevatedButton(
onPressed: () {
setState(() {
_left = 100;
});
},
child: const Text("AnimatedPositioned"),
),
)
],
),
),
Container(
height: 100,
color: Colors.grey,
child: AnimatedAlign(
duration: duration,
alignment: _align,
child: ElevatedButton(
onPressed: () {
setState(() {
_align = Alignment.center;
});
},
child: const Text("AnimatedAlign"),
),
),
),
AnimatedContainer(
duration: duration,
height: _height,
color: _color,
child: TextButton(
onPressed: () {
setState(() {
_height = 150;
_color = Colors.blue;
});
},
child: const Text(
"AnimatedContainer",
style: TextStyle(color: Colors.white),
),
),
),
AnimatedDefaultTextStyle(
child: GestureDetector(
child: const Text("hello world"),
onTap: () {
setState(() {
_style = const TextStyle(
color: Colors.blue,
decorationStyle: TextDecorationStyle.solid,
decorationColor: Colors.blue,
);
});
},
),
style: _style,
duration: duration,
),
AnimatedOpacity(
opacity: _opacity,
duration: duration,
child: TextButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.blue)),
onPressed: () {
setState(() {
_opacity = 0.2;
});
},
child: const Text(
"AnimatedOpacity",
style: TextStyle(color: Colors.white),
),
),
),
AnimatedDecoratedBox1(
duration: Duration(
milliseconds: _decorationColor == Colors.red ? 400 : 2000),
decoration: BoxDecoration(color: _decorationColor),
child: Builder(builder: (context) {
return TextButton(
onPressed: () {
setState(() {
_decorationColor = _decorationColor == Colors.blue
? Colors.red
: Colors.blue;
});
},
child: const Text(
"AnimatedDecoratedBox toggle",
style: TextStyle(color: Colors.white),
),
);
}),
)
].map((e) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: e,
);
}).toList(),
),
);
}
}
已有的过渡组件,可以直接通过定义参数的方式进行设置,而完全无需担心动画相关逻辑的维护,为开发带来了极大的便利。通过自定义组件,可以更深入地理解动画过渡组件的实现原理,以达到更为复杂的动画过渡效果。