46

When I use Navigator.pushNamed(context, "/someRoute");, there is a minimal animation which slides in the new route from the bottom of the screen (on Android, might look different on iOS).

How can I add a custom animation to this transition?

I found this article, which has some very neat sample code for unnamed routes. They implement their own class which inherits from PageRouteBuilder and can be used like this: Navigator.push(context, SlideRightRoute(page: Screen2())). But a PageRouteBuilder is not a Widget and can't be registered as a route in MaterialApp. So I don't see how to apply this to named routes.

lhk
  • 27,458
  • 30
  • 122
  • 201

4 Answers4

66

You need to use onGenerateRoute in your MaterialApp widget.

onGenerateRoute: (settings) {
  if (settings.name == "/someRoute") {
    return PageRouteBuilder(
      settings: settings, // Pass this to make popUntil(), pushNamedAndRemoveUntil(), works
      pageBuilder: (_, __, ___) => SomePage(),
      transitionsBuilder: (_, a, __, c) => FadeTransition(opacity: a, child: c)
    );
  }
  // Unknown route
  return MaterialPageRoute(builder: (_) => UnknownPage());
},
Ber
  • 40,356
  • 16
  • 72
  • 88
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • 2
    It worked for me. The only thing I would add is the `settings` needs to be passed to the `PageRouteBuilder` constructor so the route arguments are included. Here is what I ended up with: ``` onGenerateRoute: (settings) => PageRouteBuilder( pageBuilder: (context, _, __) => routes[settings.name](context), settings: settings, transitionsBuilder: (_, animation1, __, child) => FadeTransition(opacity: animation1, child: child), ), ``` – Your Friend Ken Oct 04 '19 at 20:32
  • 16
    Also, if you set `routes` on `MaterialApp` it seems to ignore `onGenerateRoute` – Your Friend Ken Oct 04 '19 at 20:45
  • 4
    If I use this, the url at the top no longer seems to show the new route name. Isn't that an issue if I want to say use pushNamedAndRemoveUntil()? – Eradicatore Feb 11 '20 at 13:25
  • 2
    This is the simplest and best answer on how to setup a minimal page transition. Simple is good. Thank you. – devdanke Jun 27 '20 at 17:39
  • It breaks the feel of the app though, sometimes you want to push from right sometimes you want to fade.. – Oliver Dixon May 02 '23 at 18:57
3

I found an easy solution (inspired from this code)

First, you need to set a static GlobalKey for MaterialApp and export it

static GlobalKey mtAppKey = GlobalKey();

Widget build(BuildContext context) {
  return MaterialApp(
    key: MyApp.mtAppKey,
    ...

Also, you need to a custom PageRouteBuilder to handle it

Null-safety disabled

class CustomNamedPageTransition extends PageRouteBuilder {
  CustomNamedPageTransition(
    GlobalKey materialAppKey,
    String routeName, {
    Object arguments,
  }) : super(
          settings: RouteSettings(
            arguments: arguments,
            name: routeName,
          ),
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) {
            assert(materialAppKey.currentWidget != null);
            assert(materialAppKey.currentWidget is MaterialApp);
            var mtapp = materialAppKey.currentWidget as MaterialApp;
            var routes = mtapp.routes;
            assert(routes.containsKey(routeName));
            return routes[routeName](context);
          },
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) =>
              FadeTransition(
            opacity: animation,
            child: child,
          ),
          transitionDuration: Duration(seconds: 1),
        );
}

Null-safety enabled

class CustomNamedPageTransition extends PageRouteBuilder {
  CustomNamedPageTransition(
    GlobalKey materialAppKey,
    String routeName, {
    Object? arguments,
  }) : super(
          settings: RouteSettings(
            arguments: arguments,
            name: routeName,
          ),
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) {
            assert(materialAppKey.currentWidget != null);
            assert(materialAppKey.currentWidget is MaterialApp);
            var mtapp = materialAppKey.currentWidget as MaterialApp;
            var routes = mtapp.routes;
            assert(routes!.containsKey(routeName));
            return routes![routeName]!(context);
          },
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) =>
              FadeTransition(
            opacity: animation,
            child: child,
          ),
          transitionDuration: Duration(seconds: 1),
        );
}

Then, you can open your named route with

Navigator.push(
  context,
  CustomNamedPageTransition(
    MyApp.mtAppKey,
    MyRoute.routeName,
  ),
);

or

Navigator.pushReplacement(
  context,
  CustomNamedPageTransition(
    MyApp.mtAppKey,
    MyRoute.routeName,
  ),
);
2

Using animated routes is possible without onGenerateRoute!

If you are using MaterialApp's routes map for defining your named routes, here is how you could define a named route (whose name will not be null).

Just create your route by extending PageRouteBuilder:

import 'package:flutter/material.dart';

class FadeInRoute extends PageRouteBuilder {
  final Widget page;

  FadeInRoute({this.page, String routeName})
      : super(
          settings: RouteSettings(name: routeName),            // set name here
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) =>
              page,
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) =>
              FadeTransition(
            opacity: animation,
            child: child,
          ),
          transitionDuration: Duration(milliseconds: 500),
        );
}

and then when you are navigating, just do:

Navigator.push( // or pushReplacement, if you need that
  context,
  FadeInRoute(
    routeName: RouteNames.home,
    page: MyHomeScreen(),
  ),
);
Aleksandar
  • 3,558
  • 1
  • 39
  • 42
  • 1
    Can you explain how do you pass argument `FadeInRoute` in `Navigator.pushNamed` as second parameter when `String` is expected? – Mike Kokadii Jan 28 '21 at 12:30
  • 2
    @MikeKokadii I think @Alexandar meant to use `Navigator.push()`. This method is great for transitioning to a pre-determined widget at a given routeName. However, you need to provide the Widget for that page yourself. If you are looking for an easy way to make a nice transition replacement for `Navigator.pushNamed(....)` this is not it. – Max Jan 29 '21 at 11:05
  • @Max thank you for pointing that out! It was definitely a mistake. In my original code, I used `pushReplacement` so I probably mis-read it when I was writing this. The answer is now edited :) – Aleksandar Jan 29 '21 at 14:02
  • @MikeKokadii please check if this works for you now :) – Aleksandar Jan 29 '21 at 14:03
  • @Aleksandar this class can do the `Navigator.pop` too? I'm calling my Fade class on `pop` like this: `Navigator.pop(context, FadeInRoute());` – Kaya Ryuuna Aug 02 '21 at 20:46
  • @KayaRyuuna the second argument to `pop` is the "result of the navigation", i.e. the return value of the `await`ed `push`. See [the docs](https://api.flutter.dev/flutter/widgets/Navigator/pop.html) for more info. – Aleksandar Aug 03 '21 at 09:20
0

Creating an animated transition and using popUntil with named routes does not require the use of onGenerateRoute. You only need to specify the routeName again when creating the PageRouteBuilder.

Modifying the example from the Flutter docs, maintaining a named reference to a route can be achieved by adding the settings parameter to PageRouteBuilder:

Route _createRoute() {
  return PageRouteBuilder(
    settings: RouteSettings(name: '/new-screen'),
    pageBuilder: (context, animation, _) => const NewScreen(),
    transitionsBuilder: (context, animation, _, child) {
      const begin = Offset(0.0, 1.0);
      const end = Offset.zero;
      const curve = Curves.ease;

      var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

      return SlideTransition(
        position: animation.drive(tween),
        child: child,
      );
    },
  );
}

which is simply called using Navigator.of(context).push(_createRoute());

The screen NewScreen can be normally registered under the MaterialApp routes:

 MaterialApp(
 ...
 ...
   routes: {
     ...
     '/new-screen': (context) => NewScreen(),
   }
 )

You can modify the code above to be more dynamic. That said, using onGeneratedRoute is a more permanent solution