近期在flutter开发进程中,遇到一些需求自界说转场动画的情况,就想研究一下相关的常识,有爱好的小伙伴能够跟我一起了解下这块的逻辑和用法

经过自界说转场能够完结常用的滑动转场、透明度转场、缩放转场,以及一些杂乱的复合转场等

作用如下:

一.初识

首要咱们来看下flutter常见的转场逻辑:

Navigator.of(context).push(
  MaterialPageRoute(
    builder: (context) {
      return const NewPage();
    },
  ),
);

经过push办法,传入一个MaterialPageRoute包裹的新页面,就能够完结默许的转场了(默许的从右到左,回来的时候是逆向的)

假如不想运用这种默许的转场,咱们能够经过自界说一个PageRoute,完结各式各样的转场动画

自界说PageRoute

class SlideUpPageRoute<T> extends MaterialPageRoute<T> {
  SlideUpPageRoute({
    required WidgetBuilder builder,
    RouteSettings settings,
  }) : super(builder: builder, settings: settings);
  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(0.0, 1.0),
        end: Offset.zero,
      ).animate(animation),
      child: child,
    );
  }
}

运用自界说的PageRoute完结转场:

Navigator.of(context).push(
  SlideUpPageRoute(
    builder: (context) {
      return const NewPage();
    },
  ),

要想弄清楚转场动画的具体流程,咱们需求了解路由转场是怎么完结的

假如只想了解用法,能够直接跳到第三部分:自界说转场动画

二、转场动画相关逻辑及流程

1. Navigator 路由导航

在flutter中,界面跳转是经过路由导航器Navigator来操控的

WidgetsAppMaterialApp创立和装备了一个导航器,担任办理一个[Route]目标堆栈,不需求咱们手动创立

能够经过Navigator.of(context)获取当时导航器目标,经过push和pop办法操控路由目标的进栈和出栈,完结页面的跳转

/// You can create your own subclass of one of the widget library route classes
/// like [PopupRoute], [ModalRoute], or [PageRoute], to control the animated
/// transition employed to show the route, the color and behavior of the route's
/// modal barrier, and other aspects of the route.
...
...
/// Push the given route onto the navigator that most tightly encloses the given context.
@optionalTypeArgs
static Future<T?> push<T extends Object?>(BuildContext context, Route<T> route) {
  return Navigator.of(context).push(route);
}
......
@optionalTypeArgs
Future<T?> push<T extends Object?>(Route<T> route) {
   assert(_debugCheckIsPagelessRoute(route));
   _pushEntry(_RouteEntry(route, initialState: _RouteLifecycle.push));
   return route.popped;
}

其间,RouteFlutter中通用的路由办理类, 是一个笼统类,界说了导航器和路由之间的笼统接口, 咱们能够经过Route来自界说路由的创立、毁掉、跳转等行为,而且能够依据自己的需求来完结不同的路由办理战略

页面路由目标route由两部分组成,即页面page和过渡作用transition; 页面page一般只构建一次,过渡作用transition是在每个帧的继续时刻内动态构建的

Navigator.of(context).push(PageRouteBuilder(
  opaque: false,
  pageBuilder: (BuildContext context, _, __) {
    return Center(child: Text('My PageRoute'));
  },
  transitionsBuilder: (___, Animation<double> animation, ____, Widget child) {
    return FadeTransition(
      opacity: animation,
      child: RotationTransition(
        turns: Tween<double>(begin: 0.5, end: 1.0).animate(animation),
        child: child,
      ),
    );
  }
));

过渡作用transition便是咱们这一次探求的重点,即转场动画

2. 常用路由转场类MaterialPageRouteCupertinoPageRoute

/// See [MaterialPageRoute] for a route that replaces the entire screen with a
platform-adaptive transition.

Navigator的介绍中主张参阅MaterialPageRoute来完结转场路由。那么问题来了,MaterialPageRoute是什么,又为什么要运用它来完结常用路由转场呢?

  • MaterialPageRouteCupertinoPageRoute都是Flutter供给的一个具体的路由完结类,二者作用相似,供给不同风格的转场动画,下面首要阐明MaterialPageRoute的相关内容

    在源码中介绍如下:

    /// A modal route that replaces the entire screen with a platform-adaptive transition.
    一种模式道路,用渠道自适应过渡代替整个屏幕。
    
  • MaterialPageRoute承继于笼统类PageRoute,供给了界说和办理导航栈的办法,而且会在屏幕之间进行过渡动画

  • 在不同渠道上有不同的转场动画表现,比如在iOS设备上供给了一个默许的从右向左的滑动转场,并附带二次动画和手势转场

MaterialPageRoute的初始化

MaterialPageRoute({
  required this.builder,
  RouteSettings? settings,
  this.maintainState = true,
  bool fullscreenDialog = false,
}) : assert(builder != null),
     assert(maintainState != null),
     assert(fullscreenDialog != null),
     super(settings: settings, fullscreenDialog: fullscreenDialog) {
  assert(opaque);
}
  • builder参数
    • 构建函数,回来一个Widget,即为路由跳转的具体页面,依据push和pop的时刻在不同的上下文中构建和重建
  • settings参数
    • 装备信息,用于传递路由信息,每个路由都有一个唯一的RouteSettings目标,它包含路由的称号和参数
    • 首要作用是协助Flutter在路由导航进程中正确地办理路由栈,从而完结页面跳转和回来。在运用Navigator进行页面跳转时,咱们能够经过它来传递路由参数,一起也能够经过它来获取当时路由的称号和参数
    • 另外还能够用于完结路由拦截和重定向。经过重写Navigator的onGenerateRoute办法,咱们能够在路由跳转时对路由进行拦截和修改,例如在用户没有登录的情况下,能够将路由重定向到登录页面。
  • maintainState参数
    • 默许情况下,当一个路由被另一个替换时,上一个路由将保留在内存中。若要在不需求时释放一切资源,请将其设置为false
  • fullscreenDialog参数
    • 指定传入路由是否为全屏模式对话框

3.ModalRoute 模态路由

PageRouteMaterialPageRoute的父类,ModalRoute又是PageRoute的父类,二者都是是笼统类,界说了一些路由类需求用到的的特点和办法

ModalRoute(模态路由),是一种特殊的路由,它会掩盖在前一个路由之上,而且需求用户进行一些操作后才能回来前一个路由,该笼统类界说了两个关键的办法

  • buildPage办法
    • 用于构建路由页面的Widget
  • buildTransitions办法
    • 供给自界说的页面过渡动画作用,会在模态路由进入或退出时被调用
    • 参数
      • context 上下文目标
      • animation Animation<double>类型,进入或退出的动画,当Navigator将一条道路推到其堆栈顶部时,新道路的首要动画从0.0运转到1.0。当推出栈顶时,主动画从1.0运转到0.0。
      • secondaryAnimation 辅佐动画,用于被推入界面,即该界面push到下一界面时该界面的转场动画;当Navigator将新道路推到堆栈顶部时,旧的最顶层道路的secondary动画从0.0运转到1.0。当Navigator退出最上方的道路时,其下方的道路的secondary从1.0运转到0.0
      • child 转场的子元素

buildTransitions办法决定了路由转场动画的形式

上面说到的MaterialPageRoute的默许转场动画,便是由于其buildPagebuildTransitions办法由MaterialRouteTransitionMixin混入类完结了完结

@override
Widget buildPage(
  BuildContext context,
  Animation<double> animation,
  Animation<double> secondaryAnimation,
) {
  final Widget result = buildContent(context);
  assert(() {
    if (result == null) {
      throw FlutterError(
        'The builder for route "${settings.name}" returned null.\n'
        '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 PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
  return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
}
......
......
Duration get transitionDuration => const Duration(milliseconds: 300);

假如想自界说转场路由,

  • 能够创立一个承继于PageRoute的类,并重写各项其笼统办法

    @override
    bool get opaque => true;
    @override
    bool get barrierDismissible => false;
    @override
    bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => nextRoute is PageRoute;
    @override
    bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => previousRoute is PageRoute;
    ...
    ...
    

    以及ModalRoute的笼统办法

    Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
    Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
    ) {
        return child;
    }
    
  • 也能够直接承继MaterialPageRoute类,其间对很多笼统办法已经做了完结,这样咱们只需求完结buildTransitions办法即可,主张选用这种办法

    假如想要更改转场动画时长,能够重写transitionDuration特点的get办法

    Duration get transitionDuration => const Duration(milliseconds: 300);
    
  • 也能够经过PageRouteBuilder来创立路由,作用一致

    PageRouteBuilder(
    transitionDuration: Duration(milliseconds: 500),
    pageBuilder: (_, __, ___) => NewPage(),
    transitionsBuilder: (_, animation, __, child) =>
      SlideTransition(
        position: Tween<Offset>(
          begin: Offset(0, 1),
          end: Offset.zero,
        ).animate(animation),
        child: child,
      ),
    );
    

4.TransitionRoute 转场路由

TransitionRoute类是ModalRoute的父类,也是一个笼统类,是Flutter中路由动画的基础,它为咱们供给了一个一致的接口,能够方便地完结各种路由动画作用

上面说到的transitionDuration转场时刻便是在这儿界说的,甚至能够修改回来动画的转场时刻reverseTransitionDuration与进入动画不一致

进入动画animation和二次动画secondaryAnimation的动画目标也在这儿界说

Duration get transitionDuration;
Duration get reverseTransitionDuration => transitionDuration;
Animation<double>? get animation => _animation;
Animation<double>? _animation;
Animation<double>? get secondaryAnimation => _secondaryAnimation;
final ProxyAnimation _secondaryAnimation = ProxyAnimation(kAlwaysDismissedAnimation);

此外在这个类里边还界说了动画操控器_controller,担任操控动画进度

5.OverlayRoute 悬浮窗口路由

OverlayRoute类是TransitionRoute的父类,相同也是一个笼统类,首要作用是在当时Widget树上增加一个悬浮窗口,路由的转场基于这个功能完结,

能够经过调用Navigator.of(context).push办法来显现OverlayRoute,调用Navigator.of(context).pop办法来封闭OverlayRoute

综上:路由类的层级为 MaterialPageRoute/CupertinoPageRoute => PageRoute => ModalRoute => TransitionRoute => OverlayRoute => Route

三、自界说转场动画

嘿,醒醒,擦擦口水,总算到了咱们最关心的转场动画完结的环节了

像上面说到的,自界说转场动画,需求完结buildTransitions办法,里边依据传入的Animation目标来构建动画的具体完结Widget

关于转场动画动画的部分,能够运用AnimationAnimatedWidget来完结

  • Animation
    • 一个表明动画的笼统类,它包含有关动画的状况和进度信息,Animation目标自身并不能完结动画作用
    • 一般运用Tween目标来界说动画的开端和结束状况,并依据一些插值器计算动画的中间状况
  • AnimatedWidget
    • 一个用于动画作用的笼统类,当Listenable参数发生改变时,会重新构建。
    • 能够运用Animation目标的值来构建其自己的UI

AnimatedWidget是一个笼统类,具体能够运用它的一些子类Widget,例如FadeTransition、SlideTransition等,也能够是自界说的动画AnimatedBuilder。

1.体系供给的转场动画类

可用于转场的动画类

  • SlideTransition 滑动动画
  • ScaleTransition 缩放动画
  • RotationTransition 旋转动画
  • FadeTransition 透明度动画

用于小部件内动画的类

  • PositionedTransition 方位动画
  • RelativePositionedTransition 相对方位动画
  • SizeTransition 尺度巨细动画
  • AlignTransition 方位改变动画
  • SliverFadeTransition Sliver过渡动画
  • DefaultTextStyleTransition 默许文本样式过渡
  • DecoratedBoxTransition 装修盒子过渡

下面来介绍一下这几种转场动画的用法

1.1 SlideTransition 滑动转场动画

const SlideTransition({
  Key? key,
  required Animation<Offset> position,
  this.transformHitTests = true,
  this.textDirection,
  this.child,
}) : assert(position != null),
     super(key: key, listenable: position);

监测目标为positio动画,包含方位信息

  • transformHitTests参数 用于指示命中测试是否应该转化为父级坐标系中的坐标

  • textDirection参数 文本方向

能够用Tween来完结,用于在一段时刻内从一个值转化到另一个值的类,它能够用于动画作用、突变作用等,支撑不同类型的值改变,如Color、Offset等等。

 @override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(0.0, 1.0),
        end: Offset.zero,
      ).chain(CurveTween(curve: Curves.ease)).animate(animation),
      child: child,
    );
  }

这儿咱们经过animation特点创立了一个从下到上,先加快后减速的转场动画.

flutter如何自定义转场动画

上面部分有说到secondaryAnimation是辅佐动画, 用于被推入界面,即该界面push到下一界面时的转场动画, 咱们来试一下作用

@override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(-1.0, 0),
        end: Offset.zero,
      ).animate(animation),
      child: SlideTransition(
        position: Tween<Offset>(
          begin: Offset.zero,
          end: const Offset(1.0, 0),
        ).animate(secondaryAnimation),
        child: child,
      ),
    );
  }

这儿创立的还是从下到上的转场动画,可是当新push的界面再次发生转场时,该界面会呈现一个从上到底部的转场。

即A界面push到B时,B界面履行从右到左的侧滑动画,当B界面push到C时,B界面从顶部滑动到底部消失,一起C界面履行自己的转场动画

flutter如何自定义转场动画
ng动画

1.2 ScaleTransition 缩放转场动画

const ScaleTransition({
  super.key,
  required Animation<double> scale,
  this.alignment = Alignment.center,
  this.filterQuality,
  this.child,
}) : assert(scale != null),
     super(listenable: scale);

监测目标为scale动画,包含缩放份额信息

  • alignment参数为缩放对齐办法

  • filterQuality参数为图画过滤质量

@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
  return ScaleTransition(
    scale: Tween<double>(
      begin: 2.0,
      end: 1.0,
    ).chain(CurveTween(curve: Curves.ease)).animate(animation),
    alignment: Alignment.topRight,
    child: child,
  );
}

能够经过以上办法完结一个以右上角为基点,从两倍视图缩放到正常巨细的转场动画

flutter如何自定义转场动画

1.3 RotationTransition旋转转场动画

const RotationTransition({
  super.key,
  required Animation<double> turns,
  this.alignment = Alignment.center,
  this.filterQuality,
  this.child,
}) : assert(turns != null),
     super(listenable: turns);

监测目标为turns动画,包含旋转的视点信息,1.0表明旋转360度, 0.5表明旋转180度

  • alignment:旋转中心点的对齐办法,默许中心对齐

  • filterQuality参数为图画过滤质量

@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
  return RotationTransition(
    turns: Tween<double>(
      begin: 0.5,
      end: 0,
    ).chain(CurveTween(curve: Curves.ease)).animate(animation),
    alignment: Alignment.topLeft,
    child: child,
  );
}

这儿完结了一个180度,从左上角旋转进入的转场动画

flutter如何自定义转场动画

1.4 FadeTransition 透明度转场动画

const FadeTransition({
    Key? key,
    required this.opacity,
    this.alwaysIncludeSemantics = false,
    Widget? child,
  }) : assert(opacity != null),
       super(key: key, child: child);

监测目标为opacity动画,包含透明度信息,

  • alwaysIncludeSemantics: 用于操控是否将子组件的语义信息始终包含在FadeTransition的语义树中,参数为true时,不管子组件的透明度是多少,子组件的语义信息始终会被包含在FadeTransition的语义树中,这意味着子组件在语义上仍然是可拜访的。当alwaysIncludeSemantics参数为false时,子组件在不可见时也不会被包含在语义树中,因而无法被拜访。
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
  return FadeTransition(
    opacity: Tween<double>(
      begin: 0,
      end: 1,
    ).chain(CurveTween(curve: Curves.ease)).animate(animation),
    child: child,
  );
}

这儿完结了一个透明度突变显现从0到1的转场动画

flutter如何自定义转场动画

2.界面内转场动画

上面说到的都是路由转场时自界说的动画,咱们在这儿简称为路由转场动画

除了路由转场动画外,在界面build时也能够增加自界说的转场动画,为了便于区别,咱们称之为界面内转场动画

界面内转场动画与路由转场动画彼此独立,但与路由转场动画一致的是,界面内转场动画也仅会在界面路由push和pop时收效

界面内转场动画能够运用ModalRoute.of(context)!.animation获取对应的转场动画进度相关信息来完结

下面完结了一个界面呈现时,除了路由转场动画外,还有一个逐渐放大的图片的界面转场动画

@override
  Widget build(BuildContext context) {
    return Center(
      child: SizeTransition(
        sizeFactor: Tween<double>(begin: 0, end: 1.0).animate(
          CurvedAnimation(
            parent: ModalRoute.of(context)!.animation!,
            curve: Curves.easeInOut,
          ),
        ),
        child: Scaffold(
          appBar: AppBar(
            title: const Text('子部件尺度动画转场界面'),
          ),
          body: const Icon(
            Icons.flutter_dash,
            size: 400,
          ),
        ),
      ),
    );
  }

路由转场这儿咱们运用自界说的无动画作用的转场,也能够结合上述的各种动画作用来完结

@override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return child;
  }

flutter如何自定义转场动画

界面内自界说转场动画灵敏度较高,能够运用体系供给的各种动画Widget,也能够进行各种自界说

3.AnimatedBuilder完结自界说动画

关于触及额外状况的更杂乱情况,请考虑运用AnimatedBuilder。

const AnimatedBuilder({
  super.key,
  required Listenable animation,
  required this.builder,
  this.child,
}) : assert(animation != null),
     assert(builder != null),
     super(listenable: animation);
  • animation: 监听动画,值发生改变时调用builder函数
  • builder: 创立函数,将接收一个BuildContext和一个Widget参数,该Widget参数将在每次调用时重建并更新其状况
@override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return AnimatedBuilder(
      animation: animation,
      builder: (BuildContext context, Widget? child) {
        return Transform.rotate(
          angle: animation.value * pi * 2,
          child: Transform.scale(
            scale: animation.value,
            child: child
          ),
        );
      },
      child: child,
    );
  }

上面咱们完结了一个旋转+缩放的动画转场作用

flutter如何自定义转场动画

@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
  return AnimatedBuilder(
    animation: animation,
    builder: (BuildContext context, Widget? child) {
      return Transform.translate(
        offset: Offset((1 - animation.value) * 375, (1 - animation.value) * 500),
        child: Transform.scale(
          scale: animation.value,
          child: Opacity(
              opacity: animation.value,
              child: child,
            ),
        ),
      );
    },
    child: child,
  );
}

上面完结了一个方位改换+渐显动画转场作用

flutter如何自定义转场动画

四、拓宽

转场进程增加手势

在iOS机型的日常运用进程中,咱们会发现flutter供给的默许转场,支撑经过手势进行侧滑回来,以及取消转场等,跟原生的转场作用相似

这些是经过CupertinoPageRoute混入CupertinoRouteTransitionMixin类来完结的

 static bool isPopGestureInProgress(PageRoute<dynamic> route) {
    return route.navigator!.userGestureInProgress;
  }
  bool get popGestureInProgress => isPopGestureInProgress(this);
  bool get popGestureEnabled => _isPopGestureEnabled(this);
  static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
      ...
      ...
  }
  static _CupertinoBackGestureController<T> _startPopGesture<T>(PageRoute<T> route) {
    assert(_isPopGestureEnabled(route));
    return _CupertinoBackGestureController<T>(
      navigator: route.navigator!,
      controller: route.controller!, // protected access
    );
  }

popGestureInProgress特点用于指示是否正在进行手势回来操作

_isPopGestureEnabled办法用于操控Cupertino页面路由转化中手势回来操作的启用状况

_startPopGesture办法是开端手势回来操作

其间经过手势进行回来转场的核心逻辑是在buildPageTransitions办法中经过_CupertinoBackGestureDetector完结的

static Widget buildPageTransitions<T>(
    PageRoute<T> route,
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    // Check if the route has an animation that's currently participating
    // in a back swipe gesture.
    //
    // In the middle of a back gesture drag, let the transition be linear to
    // match finger motions.
    final bool linearTransition = isPopGestureInProgress(route);
    if (route.fullscreenDialog) {
      return CupertinoFullscreenDialogTransition(
        primaryRouteAnimation: animation,
        secondaryRouteAnimation: secondaryAnimation,
        linearTransition: linearTransition,
        child: child,
      );
    } else {
      return CupertinoPageTransition(
        primaryRouteAnimation: animation,
        secondaryRouteAnimation: secondaryAnimation,
        linearTransition: linearTransition,
        child: _CupertinoBackGestureDetector<T>(
          enabledCallback: () => _isPopGestureEnabled<T>(route),
          onStartPopGesture: () => _startPopGesture<T>(route),
          child: child,
        ),
      );
    }
  }

其核心内容是依据手势拖拽的间隔换算成份额,来设置TransitionRoute类供给的controller动画目标的值,从而完结手势对转场进度的操控

具体代码由于具体篇幅较长就不在文章内粘出了,有爱好的能够参阅下demo

完结了一个手势操控透明度转场,作用如下:

参阅:

Flutter转场动画 SlideTransition

Flutter 转场动效大合集