实际开发中,我们经常会遇到切换UI元素的场景,比如Tab切换、路由切换。为了增强用户体验,通常在切换时都会指定一个动画,以使切换过程显得平滑。Flutter SDK组件库中已经提供了一些常用的切换组件,如PageView、TabView等,但是,这些组件并不能覆盖全部的需求场景,为此,Flutter SDK中提供了一个AnimatedSwitcher组件,它定义了一种通用的UI切换抽象
1. AnimatedSwitcher
AnimatedSwitcher 可以同时对其新、旧子元素添加显示、隐藏动画。也就是说在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)
这样一下,便实现了对新、旧child的动画绑定。AnimatedSwitcher的默认值是AnimatedSwitcher.defaultTransitionBuilder :
static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: child,
);
}
可以看到,返回了FadeTransition对象,也就是说默认情况,AnimatedSwitcher会对新旧child执行“渐隐”和“渐显”动画
2. AnimatedSwitcher 示例
实现一个计数器,然后在每一次自增的过程中,旧数字执行缩小动画隐藏,新数字执行放大动画显示,代码如下
class MSAnimatedSwiterDemo extends StatefulWidget {
const MSAnimatedSwiterDemo({Key? key}) : super(key: key);
@override
State<MSAnimatedSwiterDemo> createState() => _MSAnimatedSwiterDemoState();
}
class _MSAnimatedSwiterDemoState extends State<MSAnimatedSwiterDemo> {
var _count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: AnimatedSwitcher(
child: Text(
"$_count",
textScaleFactor: 1.5,
key: ValueKey<int>(_count), // 显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
),
duration: Duration(milliseconds: 500),
transitionBuilder: (child, anim1) {
return ScaleTransition(child: child, scale: anim1);
},
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
setState(() {
_count += 1;
});
},
),
);
}
}
3. AnimatedSwitcher实现原理
实际上,AnimatedSwitcher的实现原理是比较简单的,我们根据AnimatedSwitcher的使用方式也可以猜个大概。要想实现新旧 child 切换动画,只需要明确两个问题:
动画执行的时机是什么时候?
如何对新旧child执行动画?
从AnimatedSwitcher的使用方式我们可以看到:当child发生变化时(子 widget 的 key 或类型不同时则认为发生变化),则重新会重新执行build,然后动画开始执行。
我们可以通过继承 StatefulWidget 来实现AnimatedSwitcher,具体做法是在didUpdateWidget 回调中判断其新旧 child 是否发生变化,如果发生变化,则对旧 child 执行反向退场(reverse)动画,对新child执行正向(forward)入场动画即可。下面是AnimatedSwitcher实现的部分核心伪代码
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;
}
上面伪代码展示了AnimatedSwitcher实现的核心逻辑,当然AnimatedSwitcher真正的实现比这个复杂,它可以自定义进退场过渡动画以及执行动画时的布局等。
4. AnimatedCrossFade
AnimatedCrossFade可以切换两个子元素,切换过程执行渐隐渐显的动画,和AnimatedSwitcher不同的是AnimatedCrossFade是针对两个子元素,而AnimatedSwitcher是在一个子元素的新旧值之间切换。AnimatedCrossFade实现原理也比较简单,和AnimatedSwitcher类似
当AnimatedCrossFade属性值不同时,动画会自动触发。
class MSAnimatedCrossFadeDemo extends StatefulWidget {
const MSAnimatedCrossFadeDemo({Key? key}) : super(key: key);
@override
State<MSAnimatedCrossFadeDemo> createState() =>
_MSAnimatedCrossFadeDemoState();
}
class _MSAnimatedCrossFadeDemoState extends State<MSAnimatedCrossFadeDemo> {
var _first = true;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AnimatedCrossFade(
firstChild: Image.asset(
"assets/images/1.jpeg",
width: 200,
height: 200,
fit: BoxFit.cover,
key: ValueKey("first"),
),
secondChild: Image.asset(
"assets/images/2.jpeg",
width: 300,
height: 300,
fit: BoxFit.cover,
key: ValueKey("second"),
),
crossFadeState:
_first ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: Duration(milliseconds: 500),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.change_circle),
onPressed: () {
setState(() {
_first = !_first;
});
},
),
);
}
}
5. AnimatedSwitcher高级用法
5.1 左出右入
假设现在我们想实现一个类似路由平移切换的动画:旧页面屏幕中向左侧平移退出,新页面从屏幕右侧平移进入
代码如下:
class MSAnimatedSwitcherDemo1 extends StatefulWidget {
const MSAnimatedSwitcherDemo1({Key? key}) : super(key: key);
@override
State<MSAnimatedSwitcherDemo1> createState() =>
_MSAnimatedSwitcherDemo1State();
}
class _MSAnimatedSwitcherDemo1State extends State<MSAnimatedSwitcherDemo1> {
var _count = 0;
@override
Widget build(BuildContext context) {
final _text = _count < 10 ? "0$_count" : "$_count";
return Scaffold(
body: Container(
alignment: Alignment.center,
child: AnimatedSwitcher(
child: Text(
_text,
textScaleFactor: 1.5,
key: ValueKey(_text),
),
transitionBuilder: (child, anim) {
var tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0));
return SlideTransition(
position: tween.animate(anim),
child: child,
);
},
duration: Duration(milliseconds: 500),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
setState(() {
_count++;
});
},
),
);
}
}
很明显上述效果与我们所预想的有所差距
AnimatedSwitcher的 child 切换时会对新child执行正向动画(forward),而对旧child执行反向动画(reverse),所以真正的效果便是:新 child 确实从屏幕右侧平移进入了,但旧child却会从屏幕右侧(而不是左侧)退出
优化
究其原因,就是因为同一个Animation正向(forward)和反向(reverse)是对称的。所以如果我们可以打破这种对称性,那么便可以实现这个功能了。
下面我们来封装一个MSSlideTransition,它与SlideTransition唯一的不同就是对动画的反向执行进行了定制(从左边滑出隐藏)
自定义 MSSlideTransition
class MSSlideTransition extends AnimatedWidget {
MSSlideTransition({
required this.child,
required Animation<Offset> position,
this.transformHitTests = true,
}) : super(listenable: position);
final bool transformHitTests;
final Widget child;
@override
Widget build(BuildContext context) {
final position = listenable as Animation<Offset>;
Offset offset = position.value;
if (position.status == AnimationStatus.reverse) {
offset = Offset(-offset.dx, offset.dy);
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
调用时,将SlideTransition替换成MSSlideTransition即可:
...
AnimatedSwitcher(
child: Text(
_text,
textScaleFactor: 1.5,
key: ValueKey(_text),
),
transitionBuilder: (child, anim1) {
var tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));
return MSSlideTransition(
child: child, position: tween.animate(anim1));
},
duration: Duration(milliseconds: 300),
),
...
完整代码
class MSAnimatedSwitcherDemo2 extends StatefulWidget {
const MSAnimatedSwitcherDemo2({Key? key}) : super(key: key);
@override
State<MSAnimatedSwitcherDemo2> createState() =>
_MSAnimatedSwitcherDemo2State();
}
class _MSAnimatedSwitcherDemo2State extends State<MSAnimatedSwitcherDemo2> {
var _count = 0;
@override
Widget build(BuildContext context) {
final _text = _count < 10 ? "0$_count" : "$_count";
return Scaffold(
body: Container(
alignment: Alignment.center,
child: AnimatedSwitcher(
child: Text(
_text,
textScaleFactor: 1.5,
key: ValueKey(_text),
),
transitionBuilder: (child, anim1) {
var tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));
return MSSlideTransition(
child: child, position: tween.animate(anim1));
},
duration: Duration(milliseconds: 300),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
setState(() {
_count++;
});
},
),
);
}
}
class MSSlideTransition extends AnimatedWidget {
MSSlideTransition({
required this.child,
required Animation<Offset> position,
this.transformHitTests = true,
}) : super(listenable: position);
final bool transformHitTests;
final Widget child;
@override
Widget build(BuildContext context) {
final position = listenable as Animation<Offset>;
Offset offset = position.value;
if (position.status == AnimationStatus.reverse) {
offset = Offset(-offset.dx, offset.dy);
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
可以看到,我们通过这种巧妙的方式实现了类似路由进场切换的动画,实际上Flutter路由切换也正是通过AnimatedSwitcher来实现的
5.2 自定义 MSSlideTransitionX
封装一个SlideTransitionX,来实现 “左出右入”、“左入右出”、“上入下出”或者 “下入上出”动画
自定义 MSSlideTransitionX
class MSSlideTransitionX extends AnimatedWidget {
MSSlideTransitionX({
this.dir = AxisDirection.down,
required Animation<double> position,
required this.child,
this.transformHitTests = true,
}) : super(listenable: position) {
switch (dir) {
case AxisDirection.left:
// 左出右进
_tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0));
break;
case AxisDirection.right:
// 右出左进
_tween = Tween<Offset>(begin: Offset(-1, 0), end: Offset(0, 0));
break;
case AxisDirection.up:
// 上出下进
_tween = Tween<Offset>(begin: Offset(0, 1), end: Offset(0, 0));
break;
case AxisDirection.down:
// 下出上进
_tween = Tween<Offset>(begin: Offset(0, -1), end: Offset(0, 0));
break;
default:
}
}
final Widget child;
final bool transformHitTests;
final AxisDirection dir;
late final Tween<Offset> _tween;
@override
Widget build(BuildContext context) {
final position = listenable as Animation<double>;
Offset offset = _tween.evaluate(position);
if (position.status == AnimationStatus.reverse) {
switch (dir) {
case AxisDirection.left:
// 左出右进
offset = Offset(-offset.dx, offset.dy);
break;
case AxisDirection.right:
// 右出左进
offset = Offset(-offset.dx, offset.dy);
break;
case AxisDirection.up:
// 上出下进
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.down:
// 下出上进
offset = Offset(offset.dx, -offset.dy);
break;
default:
}
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
示例
class MSAnimatedSwitcherDemo3 extends StatefulWidget {
const MSAnimatedSwitcherDemo3({Key? key}) : super(key: key);
@override
State<MSAnimatedSwitcherDemo3> createState() =>
_MSAnimatedSwitcherDemo3State();
}
class _MSAnimatedSwitcherDemo3State extends State<MSAnimatedSwitcherDemo3> {
var _count = 0;
@override
Widget build(BuildContext context) {
final _text = _count < 10 ? "0$_count" : "$_count";
return Scaffold(
body: Container(
alignment: Alignment.center,
child: AnimatedSwitcher(
child: Text(
_text,
textScaleFactor: 1.5,
key: ValueKey(_text),
),
transitionBuilder: (child, anim1) {
return MSSlideTransitionX(
position: anim1,
child: child,
dir: AxisDirection.down,
);
},
duration: Duration(milliseconds: 500),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
setState(() {
_count++;
});
},
),
);
}
}
class MSSlideTransitionX extends AnimatedWidget {
MSSlideTransitionX({
this.dir = AxisDirection.down,
required Animation<double> position,
required this.child,
this.transformHitTests = true,
}) : super(listenable: position) {
switch (dir) {
case AxisDirection.left:
// 左出右进
_tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0));
break;
case AxisDirection.right:
// 右出左进
_tween = Tween<Offset>(begin: Offset(-1, 0), end: Offset(0, 0));
break;
case AxisDirection.up:
// 上出下进
_tween = Tween<Offset>(begin: Offset(0, 1), end: Offset(0, 0));
break;
case AxisDirection.down:
// 下出上进
_tween = Tween<Offset>(begin: Offset(0, -1), end: Offset(0, 0));
break;
default:
}
}
final Widget child;
final bool transformHitTests;
final AxisDirection dir;
late final Tween<Offset> _tween;
@override
Widget build(BuildContext context) {
final position = listenable as Animation<double>;
Offset offset = _tween.evaluate(position);
if (position.status == AnimationStatus.reverse) {
switch (dir) {
case AxisDirection.left:
// 左出右进
offset = Offset(-offset.dx, offset.dy);
break;
case AxisDirection.right:
// 右出左进
offset = Offset(-offset.dx, offset.dy);
break;
case AxisDirection.up:
// 上出下进
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.down:
// 下出上进
offset = Offset(offset.dx, -offset.dy);
break;
default:
}
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
https://book.flutterchina.club/chapter9/animated_switcher.html