实际开发中,经常会遇到切换UI元素的场景,比如Tab切换,路由切换。为了增强用户体验,通常在切换时都会指定一个动画,以使切换过程显的平滑。为此Flutter SDK中提供了一个AnimatedSwitcher组件,定义了一种通用的UI切换抽象。
AniamtedSwitcher
简介
AnimatedSwitcher 可以同时对其新旧子元素添加显示、隐藏动画,也就是在AnimatedSwitcher的子元素发生变化时,会对其旧元素和新元素做动画:
const AnimatedSwitcher({
Key? key,
this.child,
required this.duration, // 新child显示动画时长
this.reverseDuration,// 旧child隐藏的动画时长
this.switchInCurve = Curves.linear, // 新child显示的动画曲线
this.switchOutCurve = Curves.linear,// 旧child隐藏的动画曲线
this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // 动画构建器
this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, //布局构建器
})
当AnimatedSwitcher的child发生变化时(类型或key不同),旧child会执行隐藏动画,新child会执行显示动画。究竟执行和中动画效果则由transitionBuilder参数决定,该参数接受一个AnimatedSwitcherTransitionBuilder类型的builder:
typedef AnimatedSwitcherTransitionBuilder =
Widget Function(Widget child, Animation<double> animation);
该builder在AnimatedSwitcher的child切换时会分别对新旧child绑定动画:
- 对旧child,绑定的动画会反向执行(reverse)。
- 对新child,绑定的动画会正向执行(forward)。
AnimatedSwitcher的默认值是:AnimatedSwitcher.defaultTransitionBuilder:
Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: child,
);
}
返回一个FadeTransition对象,也就是默认情况AnimatedSwitcher会对新旧child执行渐隐渐显动画。
示例:
class SSLSwitcherAnimateRoute extends StatefulWidget{
const SSLSwitcherAnimateRoute({Key? key}):super(key: key);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return SSLSwitcherAniamteRouteState();
}
}
class SSLSwitcherAniamteRouteState extends State<SSLSwitcherAnimateRoute>{
int count = 0;
@override
Widget build(BuildContext context) {
// TODO: implement build
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation){
return ScaleTransition(scale: animation, child: child,);
},
child: Text(
"$count",
key: ValueKey<int>(count),
style: Theme.of(context).textTheme.headlineMedium,
),
),
ElevatedButton(
onPressed: (){
setState(() {
count += 1;
});
},
child: const Text("+1")
),
],
),
);
}
}
AnimatedSwitcher 原理
两个问题:
- 动画执行的时机?
- 如何对新旧动画执行动画?
从AnimatedSwitcher的使用可以看到:当child发生变化时(子widget的key类型不同时则默认发生变化),则重新执行build然后开始动画执行。
可以通过继承StatefulWidget来实现AnimatedSwitcher,具体做法是在didUpdateWidget回调中判断其新旧child是否发生变化,如果发生变化,则对旧child执行反向退场动画(reverse),对新动画执行正向动画(forward)。
核心伪代码:
Widget _widget;
void didUpdateWidget(AnimatedSwitcher oldWidget) {
super.didUpdateWidget(oldWidget);
// 检查新旧child是否发生变化(key和类型同时相等则返回true,认为没变化)
if (Widget.canUpdate(widget.child, oldWidget.child)) {
// child没变化,...
} else {
//child发生了变化,构建一个Stack来分别给新旧child执行动画
_widget= Stack(
alignment: Alignment.center,
children:[
//旧child应用FadeTransition
FadeTransition(
opacity: _controllerOldAnimation,
child : oldWidget.child,
),
//新child应用FadeTransition
FadeTransition(
opacity: _controllerNewAnimation,
child : widget.child,
),
]
);
// 给旧child执行反向退场动画
_controllerOldAnimation.reverse();
//给新child执行正向入场动画
_controllerNewAnimation.forward();
}
}
//build方法
Widget build(BuildContext context){
return _widget;
}
Flutter中还提供了一个AnimatedCrossFade组件,可以切换两个子元素,切换过程执行渐隐渐显动画,和Animated Switcher不同的是AnimatedCrossFade是针对两个子元素,而AnimatedSwitcher是在一个子元素的新旧值之间切换,AnimatedCrossFade的实现原理和AnimatedSwitcher类似。
AnimatedSwitcher高级用法
如果要实现一个路由平移切换的动画,旧页面从屏幕中左侧平移出去,新页面从屏幕右侧平移进入,用AnimatedSwitcher的话会出现问题,因为动画是一个正向一个反向进行,属于对称进行,而不是想要的平移进行。可以封装一下打破这种对称性。
class SSLSlideTransition extends AnimatedWidget{
final bool transformHitTests;
final Widget child;
const SSLSlideTransition({
Key? key,
required Animation<Offset> position,
this.transformHitTests = true,
required this.child,
}):super(key: key, listenable: position);
@override
Widget build(BuildContext context) {
// TODO: implement build
final position = listenable as Animation<Offset>;
Offset offset = position.value;
//从左向右平移
if (position.status == AnimationStatus.forward){
offset = Offset(-offset.dx, offset.dy);
}
//从右向左平移
if (position.status == AnimationStatus.reverse){
offset = Offset(-offset.dx, offset.dy);
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
//使用
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation){
// return ScaleTransition(scale: animation, child: child,);
var tween = Tween<Offset>(
begin: const Offset(1,0),
end: const Offset(0,0),
);
return SSLSlideTransition(position: tween.animate(animation), child: child);
},
child: Text(
"$count",
key: ValueKey<int>(count),
style: Theme.of(context).textTheme.headlineMedium,
),
),
SlideTransitionX
代码升级优化,实现可以上下左右平移:
class SSLSlideTransition extends AnimatedWidget{
final bool transformHitTests;
final Widget child;
final AxisDirection direction;
late final Tween<Offset> tween;
SSLSlideTransition({
Key? key,
required Animation<double> position,
this.transformHitTests = true,
this.direction = AxisDirection.down,//将参数变为枚举类型
required this.child,
}):super(key: key, listenable: position){
//构造函数中根据变量修改参数,此时不能使用const修饰构造函数否则会报错
switch (direction){
case AxisDirection.up:
tween = Tween(begin: const Offset(0, 1), end: const Offset(0, 0));
break;
case AxisDirection.right:
tween = Tween(begin: const Offset(-1, 0), end: const Offset(0, 0));
break;
case AxisDirection.down:
tween = Tween(begin: const Offset(0, -1), end: const Offset(0, 0));
break;
case AxisDirection.left:
tween = Tween(begin: const Offset(1, 0), end: const Offset(0, 0));
break;
}
}
@override
Widget build(BuildContext context) {
// TODO: implement build
final position = listenable as Animation<double>;
Offset offset = tween.evaluate(position);
if (position.status == AnimationStatus.reverse){
switch (direction){
case AxisDirection.up:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.right:
offset = Offset(-offset.dx, offset.dy);
break;
case AxisDirection.down:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.left:
offset = Offset(-offset.dx, offset.dy);
break;
}
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
//使用
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation){
// return ScaleTransition(scale: animation, child: child,);
// var tween = Tween<Offset>(
// begin: const Offset(1,0),
// end: const Offset(0,0),
// );
return SSLSlideTransition(direction: AxisDirection.up, position: animation, child: child);
},
child: Text(
"$count",
key: ValueKey<int>(count),
style: Theme.of(context).textTheme.headlineMedium,
),
),