最近有小夥伴在羣裏問「如何修改CupertinoPageRoute
進入動畫」,主要是想實現下面這個效果:git
可能有人以爲,這不就是自帶效果嗎?咱們能夠和自帶效果對比下: github
很明顯,二者的進入動畫是不同的,自帶效果默認是一個從右往左的transition。那麼,這個進入動畫能夠改嗎?CupertinoPageRoute
現有的自帶API是沒有這個接口的,因此咱們須要魔改。bash
在魔改以前,我以爲有必要講一下Flutter的路由動畫設計。在Flutter中,路由的push和pop動畫是一組的,具體體現就是:若是push動畫是Animation A
,那麼pop動畫就是Animation A.reverse()
。咱們能夠看下TransitionRoute
的源碼:less
@override
TickerFuture didPush() {
assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().');
assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
_animation.addStatusListener(_handleStatusChanged);
return _controller.forward();
}
@override
bool didPop(T result) {
assert(_controller != null, '$runtimeType.didPop called before calling install() or after calling dispose().');
assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
_result = result;
_controller.reverse();
return super.didPop(result);
}
複製代碼
很清楚,push的時候執行的是_controller.forward()
,而pop的時候執行的是_controller.reverse()
。這就解釋了爲何CupertinoPageRoute
的默認進入動畫是從右往左的一個transition了,由於側滑返回(也就是pop動畫)必定是從左往右的transition,這就決定了push動畫是從右往左了。ide
對路由動畫有了基本的瞭解之後,能夠來看下CupertinoPageRoute
的動畫設計了。CupertinoPageRoute
的繼承關係是:CupertinoPageRoute --> PageRoute --> ModalRoute --> TransitionRoute --> OverlayRoute --> Route
。CupertinoPageRoute
中,路由transition是經過buildTransitions
這個方法來建立的:動畫
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child);
}
複製代碼
這個方法的父方法源自ModalRoute
,而且在類_ModalScopeState
中被使用,咱們能夠看到頁面最終是被包裹在了一個AnimatedBuilder
控件中的,配合widget.route.buildTransitions
就能夠實現各類動畫效果了:ui
class _ModalScopeState<T> extends State<_ModalScope<T>> {
······
@override
Widget build(BuildContext context) {
return _ModalScopeStatus(
route: widget.route,
isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
canPop: widget.route.canPop, // _routeSetState is called if this updates
child: Offstage(
offstage: widget.route.offstage, // _routeSetState is called if this updates
child: PageStorage(
bucket: widget.route._storageBucket, // immutable
child: FocusScope(
node: focusScopeNode, // immutable
child: RepaintBoundary(
child: AnimatedBuilder(
animation: _listenable, // immutable
builder: (BuildContext context, Widget child) {
return widget.route.buildTransitions(
context,
widget.route.animation,
widget.route.secondaryAnimation,
IgnorePointer(
ignoring: widget.route.animation?.status == AnimationStatus.reverse,
child: child,
),
);
},
child: _page ??= RepaintBoundary(
key: widget.route._subtreeKey, // immutable
child: Builder(
builder: (BuildContext context) {
return widget.route.buildPage(
context,
widget.route.animation,
widget.route.secondaryAnimation,
);
},
),
),
),
),
),
),
),
);
}
······
}
複製代碼
那麼這個_ModalScope
是什麼時候被掛載到路由上的呢?繼續看ModalRoute
的源碼,createOverlayEntries()
中初始化了這個_ModalScope
:this
@override
Iterable<OverlayEntry> createOverlayEntries() sync* {
yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
}
Widget _buildModalScope(BuildContext context) {
return _modalScopeCache ??= _ModalScope<T>(
key: _scopeKey,
route: this,
// _ModalScope calls buildTransitions() and buildChild(), defined above
);
}
複製代碼
而createOverlayEntries()
則是在OverlayRoute
中的install()
方法中被調用的:spa
@override
void install(OverlayEntry insertionPoint) {
assert(_overlayEntries.isEmpty);
_overlayEntries.addAll(createOverlayEntries());
navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
super.install(insertionPoint);
}
複製代碼
這個install()
方法會在路由被插入進navigator的時候被調用,Flutter在這個時候填充overlayEntries,而且把它們添加到overlay中去。這個事情是由Route來作,而不是由Navigator來作是由於,Route還負責removing overlayEntries,這樣add和remove操做就是對稱的了。
上面這些綜合起來將就是:在路由intall的時候,widget.route.buildTransitions
方法給AnimatedBuilder提供了一個用來動畫的Transitions,從而使路由能動起來。
因此,要改變CupertinoPageRoute
的進入動畫,就要重寫這個widget.route.buildTransitions
方法。
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child);
}
static Widget buildPageTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
if (route.fullscreenDialog) {
return CupertinoFullscreenDialogTransition(
animation: animation,
child: child,
);
} else {
return CupertinoPageTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: isPopGestureInProgress(route),
child: _CupertinoBackGestureDetector<T>(
enabledCallback: () => _isPopGestureEnabled<T>(route),
onStartPopGesture: () => _startPopGesture<T>(route),
child: child,
),
);
}
}
複製代碼
這裏解釋下buildTransitions()
方法中的兩個參數:animation
和secondaryAnimation
。
animation
從0.0-->1.0變化;當Navigator pop最頂端的路由時(好比點擊返回鍵),animation從1.0-->0.0變化。secondaryAnimation
從0.0-->1.0變化;當路由pop最頂端路由時,secondaryAnimation從1.0-->0.0變化。 簡單來講,animation
是我本身怎麼進來和出去,而secondaryAnimation
是別人覆蓋個人時候,我怎麼進來和出去。
因此,咱們要對animation
進行一些修改,secondaryAnimation
不用管它。
class CupertinoPageTransition extends StatelessWidget {
/// Creates an iOS-style page transition.
///
/// * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0
/// when this screen is being pushed.
/// * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0
/// when another screen is being pushed on top of this one.
/// * `linearTransition` is whether to perform primary transition linearly.
/// Used to precisely track back gesture drags.
CupertinoPageTransition({
Key key,
@required Animation<double> primaryRouteAnimation,
@required Animation<double> secondaryRouteAnimation,
@required this.child,
@required bool linearTransition,
}) : assert(linearTransition != null),
_primaryPositionAnimation =
(linearTransition
? primaryRouteAnimation
: CurvedAnimation(
// The curves below have been rigorously derived from plots of native
// iOS animation frames. Specifically, a video was taken of a page
// transition animation and the distance in each frame that the page
// moved was measured. A best fit bezier curve was the fitted to the
// point set, which is linearToEaseIn. Conversely, easeInToLinear is the
// reflection over the origin of linearToEaseIn.
parent: primaryRouteAnimation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
)
).drive(_kRightMiddleTween),
_secondaryPositionAnimation =
(linearTransition
? secondaryRouteAnimation
: CurvedAnimation(
parent: secondaryRouteAnimation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
)
).drive(_kMiddleLeftTween),
_primaryShadowAnimation =
(linearTransition
? primaryRouteAnimation
: CurvedAnimation(
parent: primaryRouteAnimation,
curve: Curves.linearToEaseOut,
)
).drive(_kGradientShadowTween),
super(key: key);
// When this page is coming in to cover another page.
final Animation<Offset> _primaryPositionAnimation;
// When this page is becoming covered by another page.
final Animation<Offset> _secondaryPositionAnimation;
final Animation<Decoration> _primaryShadowAnimation;
/// The widget below this widget in the tree.
final Widget child;
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
return SlideTransition(
position: _secondaryPositionAnimation,
textDirection: textDirection,
transformHitTests: false,
child: SlideTransition(
position: _primaryPositionAnimation,
textDirection: textDirection,
child: DecoratedBoxTransition(
decoration: _primaryShadowAnimation,
child: child,
),
),
);
}
}
複製代碼
看CupertinoPageTransition
的源碼,實際上是將頁面包裹在了一個SlideTransition
中,而child是一個帶有手勢控制的_CupertinoBackGestureDetector
,這個咱們不用改,也無論它。咱們須要對SlideTransition
作一些修改,讓其在路由push的時候使用咱們自定義的transition,在pop的時候仍是保留原始的動畫和手勢控制。
明確下咱們的目的,咱們但願達成的效果是這樣的:
SlideTransition(
position: 是push嗎
? 咱們本身的push animation
: 系統自帶的_primaryPositionAnimation,
textDirection: textDirection,
child: DecoratedBoxTransition(
decoration: widget._primaryShadowAnimation,
child: widget.child,
),
),
複製代碼
因此最終須要解決的就是判斷當前是push仍是pop。我一開始是打算使用位移量來計算的,往右移就是pop,往左移就是push,可是push是帶手勢移動的,用戶能夠拉扯頁面左右瞎jb滑,因此這個方案pass;而後我換了個思路,監聽動畫的狀態,動畫結束了,就改變一下「是push嗎」這個變量的值:
@override
void initState() {
super.initState();
widget.primaryRouteAnimation.addStatusListener((status) {
print("status:$status");
if (status == AnimationStatus.completed) {
isPush = !isPush;
setState(() {
print("setState isFrom = ${isPush}");
});
}
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
return SlideTransition(
position: widget._secondaryPositionAnimation,
textDirection: textDirection,
transformHitTests: false,
child: SlideTransition(
position: isPush
? widget._primaryPositionAnimationPush
: widget._primaryPositionAnimation,
textDirection: textDirection,
child: DecoratedBoxTransition(
decoration: widget._primaryShadowAnimation,
child: widget.child,
),
),
);
}
複製代碼
其中_primaryPositionAnimationPush
就是咱們自定義的push動畫:
_primaryPositionAnimationPush = (linearTransition
? primaryRouteAnimation
: CurvedAnimation(
parent: primaryRouteAnimation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
))
.drive(_kTweenPush);
final Animatable<Offset> _kTweenPush = Tween<Offset>(
begin: Offset.zero,
end: Offset.zero,
);
複製代碼
這裏要注意下,CupertinoPageTransition
本是一個StatelessWidget
,可是咱們這裏涉及到了狀態改變,因此須要將其變爲一個StatefulWidget
。
這樣已經基本實現效果了,只是還有一個小bug,那就是用戶在滑動push的時候,若是滑到一半取消了,那麼動畫仍是會走completed的,那麼isPush狀態就不對了。咱們能夠打印下不一樣操做下primaryRouteAnimation的status,能夠發現以下結果:
這段log也側面反映了上面說的,pop動畫實際上是push動畫的reverse。咱們根據這個規修改下primaryRouteAnimation的監聽:
@override
void initState() {
super.initState();
widget.primaryRouteAnimation.addStatusListener((status) {
print("status:$status");
if (status == AnimationStatus.completed) {
isPush = false;
setState(() {
print("setState isFrom = ${isPush}");
});
} else if (status == AnimationStatus.dismissed) {
isPush = true;
setState(() {
print("setState isFrom = ${isPush}");
});
}
});
}
複製代碼
運行下,徹底符合咱們的需求。
咱們能夠修改_kTweenPush
,實現各類各樣的push變換:
從下往上:_kTweenPush = Tween(begin: const Offset(0.0, 1.0),end: Offset.zero,);
從右下往左上:_kTweenPush = Tween(begin: const Offset(1.0, 1.0),end: Offset.zero,);
而修改_kRightMiddleTween
,能夠改變pop側滑動畫,好比斜着退出: _kRightMiddleTween = Tween(begin: const Offset(1.0, 1.0),end: Offset.zero,);
反正各類騷操做,大家均可以試試。
由於CupertinoPageTransition
中已經將路由寫死爲一個SlideTransition
了,若是要實現其餘的transition,咱們須要修改build()
方法:
_primaryPositionAnimationPush = (linearTransition
? primaryRouteAnimation
: CurvedAnimation(
parent: primaryRouteAnimation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
))
.drive(Tween<double>(
begin: 0.0,
end: 1.0,
)),
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
return SlideTransition(
position: widget._secondaryPositionAnimation,
textDirection: textDirection,
transformHitTests: false,
child: isPush
? FadeTransition(
opacity: widget._primaryPositionAnimationPush,
child: widget.child,
)
: SlideTransition(
position: widget._primaryPositionAnimation,
textDirection: textDirection,
child: DecoratedBoxTransition(
decoration: widget._primaryShadowAnimation,
child: widget.child,
),
));
}
複製代碼
至於其餘的什麼大小、旋轉等等變換,本身都試試啦,藉助xxxTransition
控件都能實現。
改Duration就要方便不少了,直接重寫CupertinoPageRoute
的get transitionDuration
方法就能夠啦:
class MyCupertinoPageRoute<T> extends CupertinoPageRoute<T> {
@override
Duration get transitionDuration => const Duration(seconds: 3);
}
複製代碼