美文网首页Flutter圈子FlutterFlutter中文社区
Flutter - 路由动画|页面跳转动画 (2020.01.1

Flutter - 路由动画|页面跳转动画 (2020.01.1

作者: Cosecant | 来源:发表于2020-01-07 09:46 被阅读0次

页面跳转动画是APP必不可少的场景,曾几何时,你我搜遍了全网,找到的却是一些单页的切换动画(只有EnterPage有动画效果),或许你想反驳我,你说有人写过EnterExitPageRoute啊。但是,你在使用EnterExitPageRoute的时候,你发现Page的生命周期会出问题吗?同一个Page(实例被多次创建),initStatedidDepenciesChanged等方法被多次调用。仔细一想,这是否已经打乱了你原本的程序逻辑。。。

苦恼啊!

苦恼啊!我不仅搜遍了整个百度,甚至是整个StackOverflow,或者Medium,都只发现了EnterExitPageRoute的写法,难道他们都没发现这个类的问题吗?

嗯?

思前想后,为什么当Platform.iOS,并且设置路由为MaterialPageRoute时,我们想要的动画就浮现了,没错,我们就是想要这样的动画。那就开始研究下这个类到底被加入了什么技能。

终于,皇天不负有心人,经过一段时间的学习和研究,让我发现它动画的效果实际就是CurvedAnimationPrimrayAnimationSecondaryAnimation共同作用产生的。

让我们抛弃“EnterExitPage”的写法吧!

首先,我们先看看效果动画,这是一个水平匀速切换的动画效果:


AnimationPageRoute.gif

以下是简单的一个动画路由类,具体怎么使用我这里就暂不做介绍了。

需要注意的是每个效果切换都是指定EnterPageExitPageTween动画参数,其次是中间的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);

那先就这样吧,如果你觉得好使,就给我点个赞,谢谢!

声明:转载请注明出处,谢谢!

相关文章

网友评论

    本文标题:Flutter - 路由动画|页面跳转动画 (2020.01.1

    本文链接:https://www.haomeiwen.com/subject/tnxractx.html