在Widget属性发生变化时会执行过渡动画的组件统称为动画过渡组件,而动画过渡组件最明显的一个特征就是它会在内部自管理AnimationController。为了方便使用者可以自定义动画的曲线、执行时长、方向等,通常都需要使用者自己提供AnimationController对象来自定义这些属性值。但是如此一来,使用者就必须手动的管理AnimationController,这样会增加使用的复杂性。因此,将AnimationController进行封装,会大大提高动画组件的易用性。
自定义动画过渡组件
实现一个AnimatedDecoratedBox,它可以在decoration属性变化时,从旧状态变成新状态的过程执行一个过渡动画。
class SSLAnimatedDecoratedBox extends StatefulWidget {
final BoxDecoration decoration;
final Widget child;
final Duration duration;
final Curve curve;
final Duration? reverseDuration;
const SSLAnimatedDecoratedBox({
Key? key,
required this.decoration,
required this.child,
this.curve = Curves.linear,
required this.duration,
this.reverseDuration,
}):super(key: key);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return SSLAnimatedDecoratedBoxState();
}
}
class SSLAnimatedDecoratedBoxState extends State <SSLAnimatedDecoratedBox> with SingleTickerProviderStateMixin{
late AnimationController _controller;
late Animation<double> _animation;
late DecorationTween _tween;
@protected
AnimationController get controller => _controller;
@override
Widget build(BuildContext context) {
// TODO: implement build
return AnimatedBuilder(animation: _animation, builder: (context, child){
return DecoratedBox(
decoration: _tween.animate(_animation).value,
child: child,
);
},
child: widget.child,
);
}
@override
void initState() {
// TODO: implement initState
super.initState();
_controller = AnimationController(vsync: this,duration: widget.duration, reverseDuration: widget.reverseDuration);
_tween = DecorationTween(begin: widget.decoration);
updateCurve();
}
void updateCurve() {
_animation = CurvedAnimation(parent: _controller, curve: widget.curve);
}
@override
void didUpdateWidget(covariant SSLAnimatedDecoratedBox oldWidget) {
// TODO: implement didUpdateWidget
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() {
// TODO: implement dispose
_controller.dispose();
super.dispose();
}
}
为了方便开发者实现动画过渡组件的封装,Flutter提供了一个ImplicitlyAnimatedWidget抽象类,它继承自StatefulWidget,同时提供了一个对应的ImplicitlyAnimatedWidgetState类,AnimationController的管理就在ImplicitlyAnimatedWidgetState类中。封装时只需要分别继承ImplicitlyAnimatedWidget和ImplicitlyAnimatedWidgetState类即可:
class SSLCusAnimatedDecoratedBox extends ImplicitlyAnimatedWidget{
final BoxDecoration decoration;
final Widget child;
const SSLCusAnimatedDecoratedBox({
Key? key,
required this.decoration,
required this.child,
Curve curve = Curves.linear,
required Duration duration,
}):super(key: key, curve: curve, duration: duration);
@override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() {
// TODO: implement createState
return SSLCusAnimatedDecoratedBoxState();
}
}
class SSLCusAnimatedDecoratedBoxState extends ImplicitlyAnimatedWidgetState<SSLCusAnimatedDecoratedBox>{
late DecorationTween _decoration;
@override
Widget build(BuildContext context) {
// TODO: implement build
return DecoratedBox(decoration: _decoration.evaluate(animation),child: widget.child,);
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
// TODO: implement forEachTween
_decoration = visitor(
_decoration,
widget.decoration,
(value) => DecorationTween(begin: value),
) as DecorationTween;
}
}
其中curve、duration、reverseDuration三个属性已经定义。
可以看到State实现了build和forEachTween两个方法。在动画执行的过程中,每一帧都会调用build方法(调用逻辑在ImplicitlyAnimatedWidgetState中),所以在build中我们需要构建每一帧DecoratedBox状态,因此得算出每一帧dedecorationg状态,这个可以通过_decoration.evaluate(animation)来计算,其中animation是ImplicitlyAnimatedWidgetState积累中定义的对象,_decoration是自定义的一个DecorationTween对象。tween的主要职责就是定义动画的起始状态begin和终止状态end。对于AnimatedDecoratedBox来说,decoration的终止状态就是用户传给它的值,而起始状态是不确定的,有两种情况:
- AnimatedDecoratedBox首次build,此时直接将decoration值置为其实状态为_decoration.animate(animation),即_decoration值为DecorationTween(begin:_decoration).
- AnimatedDecoratedBox的decoration更新时,则起始状态为_decoration.animate(animation),即_decoration值为DecorationTween(begin:_decoration,animate(animation), end:decoration)。
forEachTween的作用就很明显,正是用来更新Tween的初始值,在上述来年各种功能情况下,会被调用。开发者只需要重写此方法,并在此方法中更新Tween的起始状态值即可。一些更新逻辑被屏蔽在了visitor回调中,使用时只需要传递正确的参数。看下visitor签名:
Tween<T> visitor(
Tween<T> tween, //当前的tween,第一次调用为null
T targetValue, // 终止状态
TweenConstructor<T> constructor,//Tween构造器,在上述三种情况下会被调用以更新tween
);
可以看到通过继承ImplicitlyAnimatedWidget和ImplicitlyAnimatedWidgetState类可以更快速的实现动画过渡组件的封装。
Flutter预置的动画过渡组件
| 组件名 | 功能 |
|---|---|
| AnimatedPadding | 在padding发生变化时会执行过渡动画到新状态 |
| AnimatedPositioned | 配合Stack一起使用,当定位状态发生变化时会执行过渡动画到新的状态 |
| AnimatedOpacity | 在透明opacity发生变化时执行过渡动画更新状态 |
| AnimatedAlign | 当alignment发生变化时会执行过渡动画到新的状态 |
| AnimatedContainer | 当Container属性发生变化时会执行过渡动画到新的状态 |
| AnimatedDefaultTextStyle | 当字体样式发生变化时,子组件中继承了该样式的文本组件会动态过渡到新样式 |
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(),
),
);
}
}