美文网首页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