實(shí)際開發(fā)中,我們經(jīng)常會遇到切換 UI 元素的場景,比如 Tab 切換、路由切換。為了增強(qiáng)用戶體驗(yàn),通常在切換時都會指定一個動畫,以使切換過程顯得平滑。Flutter SDK 組件庫中已經(jīng)提供了一些常用的切換組件,如PageView
、TabView
等,但是,這些組件并不能覆蓋全部的需求場景,為此,F(xiàn)lutter SDK 中提供了一個AnimatedSwitcher
組件,它定義了一種通用的UI切換抽象。
AnimatedSwitcher
可以同時對其新、舊子元素添加顯示、隱藏動畫。也就是說在AnimatedSwitcher
的子元素發(fā)生變化時,會對其舊元素和新元素,我們先看看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, // 動畫構(gòu)建器
this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, //布局構(gòu)建器
})
當(dāng)AnimatedSwitcher
的 child 發(fā)生變化時(類型或Key不同),舊 child 會執(zhí)行隱藏動畫,新child會執(zhí)行執(zhí)行顯示動畫。究竟執(zhí)行何種動畫效果則由transitionBuilder
參數(shù)決定,該參數(shù)接受一個AnimatedSwitcherTransitionBuilder
類型的builder,定義如下:
typedef AnimatedSwitcherTransitionBuilder =
Widget Function(Widget child, Animation<double> animation);
該builder
在AnimatedSwitcher
的 child 切換時會分別對新、舊child綁定動畫:
這樣一下,便實(shí)現(xiàn)了對新、舊 child 的動畫綁定。AnimatedSwitcher
的默認(rèn)值是AnimatedSwitcher.defaultTransitionBuilder
:
Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: child,
);
}
可以看到,返回了FadeTransition
對象,也就是說默認(rèn)情況,AnimatedSwitcher
會對新舊child執(zhí)行“漸隱”和“漸顯”動畫。
下面我們看一個列子:實(shí)現(xiàn)一個計(jì)數(shù)器,然后再每一次自增的過程中,舊數(shù)字執(zhí)行縮小動畫隱藏,新數(shù)字執(zhí)行放大動畫顯示,代碼如下:
import 'package:flutter/material.dart';
class AnimatedSwitcherCounterRoute extends StatefulWidget {
const AnimatedSwitcherCounterRoute({Key key}) : super(key: key);
@override
_AnimatedSwitcherCounterRouteState createState() => _AnimatedSwitcherCounterRouteState();
}
class _AnimatedSwitcherCounterRouteState extends State<AnimatedSwitcherCounterRoute> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
//執(zhí)行縮放動畫
return ScaleTransition(child: child, scale: animation);
},
child: Text(
'$_count',
//顯示指定key,不同的key會被認(rèn)為是不同的Text,這樣才能執(zhí)行動畫
key: ValueKey<int>(_count),
style: Theme.of(context).textTheme.headline4,
),
),
RaisedButton(
child: const Text('+1',),
onPressed: () {
setState(() {
_count += 1;
});
},
),
],
),
);
}
}
運(yùn)行示例代碼,當(dāng)點(diǎn)擊“+1”按鈕時,原先的數(shù)字會逐漸縮小直至隱藏,而新數(shù)字會逐漸放大,我截取了動畫執(zhí)行過程的一幀,如圖9-5所示:
上圖是第一次點(diǎn)擊“+1”按鈕后切換動畫的一幀,此時“0”正在逐漸縮小,而“1”正在“0”的中間,正在逐漸放大。
注意:AnimatedSwitcher的新舊child,如果類型相同,則Key必須不相等。
實(shí)際上,AnimatedSwitcher
的實(shí)現(xiàn)原理是比較簡單的,我們根據(jù)AnimatedSwitcher
的使用方式也可以猜個大概。要想實(shí)現(xiàn)新舊 child 切換動畫,只需要明確兩個問題:動畫執(zhí)行的時機(jī)是和如何對新舊 child 執(zhí)行動畫。從AnimatedSwitcher
的使用方式我們可以看到,當(dāng) child 發(fā)生變化時(子 widget 的key和類型不同時相等則認(rèn)為發(fā)生變化),則重新會重新執(zhí)行build
,然后動畫開始執(zhí)行。我們可以通過繼承 StatefulWidget來實(shí)現(xiàn)AnimatedSwitcher
,具體做法是在didUpdateWidget
回調(diào)中判斷其新舊 child 是否發(fā)生變化,如果發(fā)生變化,則對舊 child 執(zhí)行反向退場(reverse)動畫,對新 child 執(zhí)行正向(forward)入場動畫即可。下面是AnimatedSwitcher
實(shí)現(xiàn)的部分核心偽代碼:
Widget _widget; //
void didUpdateWidget(AnimatedSwitcher oldWidget) {
super.didUpdateWidget(oldWidget);
// 檢查新舊child是否發(fā)生變化(key和類型同時相等則返回true,認(rèn)為沒變化)
if (Widget.canUpdate(widget.child, oldWidget.child)) {
// child沒變化,...
} else {
//child發(fā)生了變化,構(gòu)建一個Stack來分別給新舊child執(zhí)行動畫
_widget= Stack(
alignment: Alignment.center,
children:[
//舊child應(yīng)用FadeTransition
FadeTransition(
opacity: _controllerOldAnimation,
child : oldWidget.child,
),
//新child應(yīng)用FadeTransition
FadeTransition(
opacity: _controllerNewAnimation,
child : widget.child,
),
]
);
// 給舊child執(zhí)行反向退場動畫
_controllerOldAnimation.reverse();
//給新child執(zhí)行正向入場動畫
_controllerNewAnimation.forward();
}
}
//build方法
Widget build(BuildContext context){
return _widget;
}
上面?zhèn)未a展示了AnimatedSwitcher
實(shí)現(xiàn)的核心邏輯,當(dāng)然AnimatedSwitcher
真正的實(shí)現(xiàn)比這個復(fù)雜,它可以自定義進(jìn)退場過渡動畫以及執(zhí)行動畫時的布局等。在此,我們刪繁就簡,通過偽代碼形式讓讀者能夠清楚看到主要的實(shí)現(xiàn)思路,具體的實(shí)現(xiàn)讀者可以參考AnimatedSwitcher
源碼。
另外,F(xiàn)lutter SDK 中還提供了一個AnimatedCrossFade
組件,它也可以切換兩個子元素,切換過程執(zhí)行漸隱漸顯的動畫,和AnimatedSwitcher
不同的是AnimatedCrossFade
是針對兩個子元素,而AnimatedSwitcher
是在一個子元素的新舊值之間切換。AnimatedCrossFade
實(shí)現(xiàn)原理比較簡單,也有和AnimatedSwitcher
類似的地方,因此不再贅述,讀者有興趣可以查看其源碼。
假設(shè)現(xiàn)在我們想實(shí)現(xiàn)一個類似路由平移切換的動畫:舊頁面屏幕中向左側(cè)平移退出,新頁面重屏幕右側(cè)平移進(jìn)入。如果要用 AnimatedSwitcher 的話,我們很快就會發(fā)現(xiàn)一個問題:做不到!我們可能會寫出下面的代碼:
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) {
var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
return SlideTransition(
child: child,
position: tween.animate(animation),
);
},
...//省略
)
上面的代碼有什么問題呢?我們前面說過在AnimatedSwitcher
的 child 切換時會分別對新 child 執(zhí)行正向動畫(forward),而對舊 child 執(zhí)行反向動畫(reverse),所以真正的效果便是:新 child 確實(shí)從屏幕右側(cè)平移進(jìn)入了,但舊 child 卻會從屏幕右側(cè)(而不是左側(cè))退出。其實(shí)也很容易理解,因?yàn)樵跊]有特殊處理的情況下,同一個動畫的正向和逆向正好是相反(對稱)的。
那么問題來了,難道就不能使用AnimatedSwitcher
了?答案當(dāng)然是否定的!仔細(xì)想想這個問題,究其原因,就是因?yàn)橥粋€Animation
正向(forward)和反向(reverse)是對稱的。所以如果我們可以打破這種對稱性,那么便可以實(shí)現(xiàn)這個功能了,下面我們來封裝一個MySlideTransition
,它與SlideTransition
唯一的不同就是對動畫的反向執(zhí)行進(jìn)行了定制(從左邊滑出隱藏),代碼如下:
class MySlideTransition extends AnimatedWidget {
MySlideTransition({
Key key,
@required Animation<Offset> position,
this.transformHitTests = true,
this.child,
})
: assert(position != null),
super(key: key, listenable: position) ;
Animation<Offset> get position => listenable;
final bool transformHitTests;
final Widget child;
@override
Widget build(BuildContext context) {
Offset offset=position.value;
//動畫反向執(zhí)行時,調(diào)整x偏移,實(shí)現(xiàn)“從左邊滑出隱藏”
if (position.status == AnimationStatus.reverse) {
offset = Offset(-offset.dx, offset.dy);
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
調(diào)用時,將SlideTransition
替換成MySlideTransition
即可:
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) {
var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
return MySlideTransition(
child: child,
position: tween.animate(animation),
);
},
...//省略
)
運(yùn)行后,我截取動畫執(zhí)行過程中的一幀,如圖9-6所示:
上圖中“0”從左側(cè)滑出,而“1”從右側(cè)滑入。可以看到,我們通過這種巧妙的方式實(shí)現(xiàn)了類似路由進(jìn)場切換的動畫,實(shí)際上 Flutter 路由切換也正是通過AnimatedSwitcher
來實(shí)現(xiàn)的。
上面的示例我們實(shí)現(xiàn)了“左出右入”的動畫,那如果要實(shí)現(xiàn)“右入左出”、“上入下出”或者 “下入上出”怎么辦?當(dāng)然,我們可以分別修改上面的代碼,但是這樣每種動畫都得單獨(dú)定義一個“Transition”,這很麻煩。本節(jié)將封裝一個通用的SlideTransitionX
來實(shí)現(xiàn)這種“出入滑動動畫”,代碼如下:
class SlideTransitionX extends AnimatedWidget {
SlideTransitionX({
Key key,
@required Animation<double> position,
this.transformHitTests = true,
this.direction = AxisDirection.down,
this.child,
})
: assert(position != null),
super(key: key, listenable: position) {
// 偏移在內(nèi)部處理
switch (direction) {
case AxisDirection.up:
_tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
break;
case AxisDirection.right:
_tween = Tween(begin: Offset(-1, 0), end: Offset(0, 0));
break;
case AxisDirection.down:
_tween = Tween(begin: Offset(0, -1), end: Offset(0, 0));
break;
case AxisDirection.left:
_tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));
break;
}
}
Animation<double> get position => listenable;
final bool transformHitTests;
final Widget child;
//退場(出)方向
final AxisDirection direction;
Tween<Offset> _tween;
@override
Widget build(BuildContext context) {
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,
);
}
}
現(xiàn)在如果我們想實(shí)現(xiàn)各種“滑動出入動畫”便非常容易,只需給direction
傳遞不同的方向值即可,比如要實(shí)現(xiàn)“上入下出”,則:
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) {
var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
return SlideTransitionX(
child: child,
direction: AxisDirection.down, //上入下出
position: animation,
);
},
...//省略其余代碼
)
運(yùn)行后,我截取動畫執(zhí)行過程中的一幀,如圖9-7所示:
上圖中“1”從底部滑出,而“2”從頂部滑入。讀者可以嘗試給SlideTransitionX
的direction
取不同的值來查看運(yùn)行效果。
本節(jié)我們學(xué)習(xí)了AnimatedSwitcher
的詳細(xì)用法,同時也介紹了打破AnimatedSwitcher
動畫對稱性的方法。我們可以發(fā)現(xiàn):在需要切換新舊 UI 元素的場景,AnimatedSwitcher
將十分實(shí)用。
更多建議: