页面跳转动画是APP必不可少的场景,曾几何时,你我搜遍了全网,找到的却是一些单页的切换动画(只有EnterPage有动画效果),或许你想反驳我,你说有人写过EnterExitPageRoute啊。但是,你在使用EnterExitPageRoute的时候,你发现Page的生命周期会出问题吗?同一个Page(实例被多次创建),initState、didDepenciesChanged等方法被多次调用。仔细一想,这是否已经打乱了你原本的程序逻辑。。。
苦恼啊!苦恼啊!我不仅搜遍了整个百度,甚至是整个StackOverflow,或者Medium,都只发现了EnterExitPageRoute的写法,难道他们都没发现这个类的问题吗?
嗯?思前想后,为什么当Platform.iOS,并且设置路由为MaterialPageRoute时,我们想要的动画就浮现了,没错,我们就是想要这样的动画。那就开始研究下这个类到底被加入了什么技能。
终于,皇天不负有心人,经过一段时间的学习和研究,让我发现它动画的效果实际就是CurvedAnimation与PrimrayAnimation及SecondaryAnimation共同作用产生的。
让我们抛弃“EnterExitPage”的写法吧!
首先,我们先看看效果动画,这是一个水平匀速切换的动画效果:
AnimationPageRoute.gif
以下是简单的一个动画路由类,具体怎么使用我这里就暂不做介绍了。
需要注意的是每个效果切换都是指定EnterPage和ExitPage的Tween动画参数,其次是中间的Curve, ReverseCurve控制着动画效果,使用的是匀速直线运动的,还是加速运动,等等。
如果你只需要有离开时的动画,而进入时不需要动画,那么则参考文章后的说明。
新增UnitaryAnimationPageRoute类来单独控制只需要EnterPage动画的路由,因为发现使用AnimationPageRoute来控制单页动画会导致ExitPage的动画一起响应,所以独立写了一个类。
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
/// Fade效果的动画参数(primary)
final Tween<double> _tweenFade = Tween<double>(begin: 0, end: 1.0);
/// 动画效果从底部到顶部的参数(primary)
final Tween<Offset> _primaryTweenSlideFromBottomToTop =
Tween<Offset>(begin: const Offset(0.0, 1.0), end: Offset.zero);
// final Tween<Offset> _secondaryTweenSlideFromBottomToTop =
// Tween<Offset>(begin: Offset.zero, end: const Offset(0.0, -1.0));
/// 动画效果从顶部到底部的参数(primary)
final Tween<Offset> _primaryTweenSlideFromTopToBottom =
Tween<Offset>(begin: const Offset(0.0, -1.0), end: Offset.zero);
// final Tween<Offset> _secondaryTweenSlideFromTopToBottom =
// Tween<Offset>(begin: Offset.zero, end: const Offset(0.0, 1.0));
/// 动画效果从右边到左边的参数(primary)
final Tween<Offset> _primaryTweenSlideFromRightToLeft =
Tween<Offset>(begin: const Offset(1.0, 0.0), end: Offset.zero);
/// 动画效果从右边到左边的参数(secondary)
final Tween<Offset> _secondaryTweenSlideFromRightToLeft =
Tween<Offset>(begin: Offset.zero, end: const Offset(-1.0, 0.0));
/// 动画效果从左边到右边的参数(primary)
final Tween<Offset> _primaryTweenSlideFromLeftToRight =
Tween<Offset>(begin: const Offset(-1.0, 0.0), end: Offset.zero);
/// 动画效果从左边到右边的参(secondary)
final Tween<Offset> _secondaryTweenSlideFromLeftToRight =
Tween<Offset>(begin: Offset.zero, end: const Offset(1.0, 0.0));
/// 动画类型枚举,`SlideRL`,`SlideLR`,`SlideTB`, `SlideBT`, `Fade`
enum AnimationType {
/// 从右到左的滑动
SlideRightToLeft,
/// 从左到右的滑动
SlideLeftToRight,
/// 从上到下的滑动
SlideTopToBottom,
/// 从下到上的滑动
SlideBottomToTop,
/// 透明过渡
Fade,
}
/// 动画路由
class AnimationPageRoute<T> extends PageRoute<T> {
AnimationPageRoute({
@required this.builder,
this.isExitPageAffectedOrNot = true,
this.animationType = AnimationType.SlideRightToLeft,
this.animationDuration = const Duration(milliseconds: 450),
RouteSettings settings,
this.maintainState = true,
bool fullscreenDialog = false,
}) : assert(builder != null),
assert(isExitPageAffectedOrNot != null),
assert(animationType != null &&
[AnimationType.SlideRightToLeft, AnimationType.SlideLeftToRight]
.contains(animationType)),
assert(maintainState != null),
assert(fullscreenDialog != null),
assert(opaque),
super(settings: settings, fullscreenDialog: fullscreenDialog);
/// 页面构造
final WidgetBuilder builder;
/// 当前页面是否有动画,默认为:`TRUE`,
/// 注意:当[AnimationType]为[SlideLeftToRight]或[SlideRightToLeft],新页面及当前页面动画均有效
final bool isExitPageAffectedOrNot;
/// 动画类型
final AnimationType animationType;
final Duration animationDuration;
@override
final bool maintainState;
@override
Duration get transitionDuration =>
animationDuration ?? const Duration(milliseconds: 450);
@override
Color get barrierColor => null;
@override
String get barrierLabel => null;
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) =>
nextRoute is AnimationPageRoute && !nextRoute.fullscreenDialog;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
final Widget result = builder(context);
assert(() {
if (result == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'The builder for route "${settings.name}" returned null.'),
ErrorDescription('Route builders must never return null.')
]);
}
return true;
}());
return Semantics(
scopesRoute: true, explicitChildNodes: true, child: result);
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
final Curve curve = Curves.linear, reverseCurve = Curves.linear;
final TextDirection textDirection = Directionality.of(context);
Tween<Offset> primaryTween = _primaryTweenSlideFromRightToLeft,
secondaryTween = _secondaryTweenSlideFromRightToLeft;
if (animationType == AnimationType.SlideLeftToRight) {
primaryTween = _primaryTweenSlideFromLeftToRight;
secondaryTween = _secondaryTweenSlideFromLeftToRight;
}
Widget enterAnimWidget = SlideTransition(
position: CurvedAnimation(
parent:
settings?.isInitialRoute == true ? secondaryAnimation : animation,
curve: curve,
reverseCurve: reverseCurve,
).drive(
settings?.isInitialRoute == true ? secondaryTween : primaryTween),
textDirection: textDirection,
child: child);
if (isExitPageAffectedOrNot != true || settings?.isInitialRoute == true)
return enterAnimWidget;
return SlideTransition(
position: CurvedAnimation(
parent: secondaryAnimation,
curve: curve,
reverseCurve: reverseCurve,
).drive(secondaryTween),
textDirection: textDirection,
child: enterAnimWidget);
}
@override
String get debugLabel => '${super.debugLabel}(${settings.name})';
}
/// 单一动画路由,指只有EnterPage才有的动画路由
class UnitaryAnimationPageRoute<T> extends PageRouteBuilder<T> {
UnitaryAnimationPageRoute({
@required this.builder,
this.animationType = AnimationType.SlideRightToLeft,
RouteSettings settings,
Duration animationDuration = const Duration(milliseconds: 300),
bool opaque = true,
bool barrierDismissible = false,
Color barrierColor,
String barrierLabel,
bool maintainState = true,
bool fullscreenDialog = false,
}) : assert(builder != null),
assert(opaque != null),
assert(barrierDismissible != null),
assert(maintainState != null),
assert(fullscreenDialog != null),
super(
settings: settings,
pageBuilder: (ctx, _, __) => builder(ctx),
transitionsBuilder: (ctx, animation, _, __) {
Widget page = builder(ctx);
switch (animationType) {
case AnimationType.Fade:
return _buildFadeTransition(animation, page);
case AnimationType.SlideBottomToTop:
case AnimationType.SlideTopToBottom:
return _buildVerticalTransition(
animation, animationType, page);
case AnimationType.SlideLeftToRight:
case AnimationType.SlideRightToLeft:
return _buildHorizontalTransition(
animation, animationType, page);
default:
return page;
}
},
transitionDuration: animationDuration,
opaque: opaque,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
barrierLabel: barrierLabel,
maintainState: maintainState,
fullscreenDialog: fullscreenDialog);
/// 页面构建
final WidgetBuilder builder;
/// 动画类型
final AnimationType animationType;
/// 构建Fade效果的动画
static FadeTransition _buildFadeTransition(
Animation<double> animation, Widget child) =>
FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
).drive(_tweenFade),
child: child);
/// 构建上下向的动画
static SlideTransition _buildVerticalTransition(
Animation<double> animation, AnimationType type, Widget child) =>
SlideTransition(
position: CurvedAnimation(
parent: animation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
).drive(type == AnimationType.SlideBottomToTop
? _primaryTweenSlideFromBottomToTop
: _primaryTweenSlideFromTopToBottom),
child: child);
/// 构建左右向的动画
static SlideTransition _buildHorizontalTransition(
Animation<double> animation, AnimationType type, Widget child) =>
SlideTransition(
position: CurvedAnimation(
parent: animation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
).drive(type == AnimationType.SlideLeftToRight
? _primaryTweenSlideFromLeftToRight
: _primaryTweenSlideFromRightToLeft),
child: child);
}
如何使用?
简要说明:
当如果路由是根路由时,此时不需要进入时的动画,只需要离开时的动画,需要设置 RouteSettings.isInitialRoute=true, 即:
settings.copyWith(isInitialRoute: true)
具体细节代码如下:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Sample Application',
debugShowCheckedModeBanner: false,
theme: AppThemeProvider.appTheme,
onGenerateRoute: _onGenerateRoute,
initialRoute: '/',
home: NotFoundPage());
/// 路由生成事件
/// + `settings` 路由配置信息
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case RouteNames.Home:
return AnimationPageRoute(
builder: (_) => HomePage(),
settings: settings.copyWith(isInitialRoute: true));
case RouteNames.Detail:
return AnimationPageRoute(
builder: (_) => AppPageRouter.DetailPage());
default: //页面未找到
return AnimationPageRoute(
builder: (_) => NotFoundPage());
}
}
}
页面跳转
Navigator.pushNamed(context, RouteNames.Detail);
那先就这样吧,如果你觉得好使,就给我点个赞,谢谢!
声明:转载请注明出处,谢谢!
网友评论