13

On Android API we can use

overridePendingTransition(int enterAnim, int exitAnim) 

to define the enter and exit transitions.

How to do it in Flutter?

I have implemented this code

class SlideLeftRoute extends PageRouteBuilder {
  final Widget enterWidget;
  SlideLeftRoute({this.enterWidget})
      : super(
      pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
        return enterWidget;
      },
      transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
        return SlideTransition(
          position: new Tween<Offset>(
            begin: const Offset(1.0, 0.0),
            end: Offset.zero,
          ).animate(animation),
          child: child
        );
      },

  );
}

but it only defines the enter transition. How can i define de exit transition?

UPDATE

Imagine that i have two screens (Screen1 and Screen2), when i execute

 Navigator.push(
        context, SlideLeftRoute(enterWidget: Screen2()));

i'd like to apply an animation to both Screen1 and Screen2 and not only to Screen2

example

Umair M
  • 10,298
  • 6
  • 42
  • 74
Guilherme Sant'Ana
  • 131
  • 1
  • 1
  • 6

5 Answers5

14

The correct way of achieving this is to use the secondaryAnimation parameter that is given in the transitionBuilder of a PageRouteBuilder object.

Here you can read more about the secondaryAnimation parameter in the documentation in the flutter/lib/src/widgets/routes.dart file in the flutter sdk:

///
/// When the [Navigator] pushes a route on the top of its stack, the
/// [secondaryAnimation] can be used to define how the route that was on
/// the top of the stack leaves the screen. Similarly when the topmost route
/// is popped, the secondaryAnimation can be used to define how the route
/// below it reappears on the screen. When the Navigator pushes a new route
/// on the top of its stack, the old topmost route's secondaryAnimation
/// runs from 0.0 to 1.0. When the Navigator pops the topmost route, the
/// secondaryAnimation for the route below it runs from 1.0 to 0.0.
///
/// The example below adds a transition that's driven by the
/// [secondaryAnimation]. When this route disappears because a new route has
/// been pushed on top of it, it translates in the opposite direction of
/// the new route. Likewise when the route is exposed because the topmost
/// route has been popped off.
///
/// ```dart
///   transitionsBuilder: (
///       BuildContext context,
///       Animation<double> animation,
///       Animation<double> secondaryAnimation,
///       Widget child,
///   ) {
///     return SlideTransition(
///       position: AlignmentTween(
///         begin: const Offset(0.0, 1.0),
///         end: Offset.zero,
///       ).animate(animation),
///       child: SlideTransition(
///         position: TweenOffset(
///           begin: Offset.zero,
///           end: const Offset(0.0, 1.0),
///         ).animate(secondaryAnimation),
///         child: child,
///       ),
///     );
///   }
/// ```

This is a working example using the secondaryAnimation parameter:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: '/',
      onGenerateRoute: (RouteSettings settings) {
        if (settings.name == '/') {
          return PageRouteBuilder<dynamic>(
            pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => Page1(),
            transitionsBuilder: (
              BuildContext context,
              Animation<double> animation,
              Animation<double> secondaryAnimation,
              Widget child,
            ) {
              final Tween<Offset> offsetTween = Tween<Offset>(begin: Offset(0.0, 0.0), end: Offset(-1.0, 0.0));
              final Animation<Offset> slideOutLeftAnimation = offsetTween.animate(secondaryAnimation);
              return SlideTransition(position: slideOutLeftAnimation, child: child);
            },
          );
        } else {
          // handle other routes here
          return null;
        }
      },
    );
  }
}

class Page1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Page 1")),
      backgroundColor: Colors.blue,
      body: Center(
        child: RaisedButton(
          onPressed: () => Navigator.push(
            context,
            PageRouteBuilder<dynamic>(
              pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => Page2(),
              transitionsBuilder: (
                BuildContext context,
                Animation<double> animation,
                Animation<double> secondaryAnimation,
                Widget child,
              ) {
                final Tween<Offset> offsetTween = Tween<Offset>(begin: Offset(1.0, 0.0), end: Offset(0.0, 0.0));
                final Animation<Offset> slideInFromTheRightAnimation = offsetTween.animate(animation);
                return SlideTransition(position: slideInFromTheRightAnimation, child: child);
              },
            ),
          ),
          child: Text("Go to Page 2"),
        ),
      ),
    );
  }
}

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Page 2"), backgroundColor: Colors.green),
      backgroundColor: Colors.green,
      body: Center(child: RaisedButton(onPressed: () => Navigator.pop(context), child: Text("Back to Page 1"))),
    );
  }
}

Result:

enter image description here

  • 2
    could you please add a third additional route, and show if you can still reproduce the secondary animation....When I push a named route it's offset is (-1, 0) not (0,0) – bihire boris Apr 29 '21 at 11:00
8

Screenshot (Null Safe):

enter image description here


I used a different way, but similar logic provided by diegodeveloper

Full code:

void main() => runApp(MaterialApp(home: Page1()));

class Page1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey,
      appBar: AppBar(title: Text('Page 1')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Navigator.push(
            context,
            MyCustomPageRoute(
              parent: this,
              builder: (context) => Page2(),
            ),
          ),
          child: Text('2nd Page'),
        ),
      ),
    );
  }
}

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blueGrey,
      appBar: AppBar(title: Text('Page 2')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Navigator.pop(context),
          child: Text('Back'),
        ),
      ),
    );
  }
}

class MyCustomPageRoute<T> extends MaterialPageRoute<T> {
  final Widget parent;

  MyCustomPageRoute({
    required this.parent,
    required WidgetBuilder builder,
    RouteSettings? settings,
  }) : super(builder: builder, settings: settings);

  @override
  Widget buildTransitions(_, Animation<double> animation, __, Widget child) {
    var anim1 = Tween<Offset>(begin: Offset.zero, end: Offset(-1.0, 0.0)).animate(animation);
    var anim2 = Tween<Offset>(begin: Offset(1.0, 0.0), end: Offset.zero).animate(animation);
    return Stack(
      children: <Widget>[
        SlideTransition(position: anim1, child: parent),
        SlideTransition(position: anim2, child: child),
      ],
    );
  }
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • It seems like with that approach or @diegodeveloper approach, the widget calls the initState() when the transition starts. Using MaterialPageRoute it does not occur. – Guilherme Sant'Ana Feb 26 '19 at 17:13
  • I've tried this code with MaterialPageRoute but initState() gets called anyway... What could it be? – magicleon94 Apr 02 '19 at 12:47
  • Do you mean `initState()` of 2nd page? – CopsOnRoad Apr 02 '19 at 14:28
  • i mean the initState of the oldWidget – Guilherme Sant'Ana Apr 04 '19 at 12:02
  • @CopsOnRoad how can i perform this animation on list item? can you guide me? – Parth Bhanderi Sep 27 '19 at 06:58
  • @Andrew The documentation is actually incorrect, for instance, they are using `TweenOffset` (which I don't know what is), it should be `Tween`, second you can't assign `AlignmentTween` to `Alignment`, third put a print statement in your `transitionsBuilder` and log what `secondaryAnimation.value` returns, etc. My answer is much cleaner to understand. – CopsOnRoad Jul 14 '21 at 06:12
7

Good question , the PageRouteBuilder use an AnimationController by default to handle the animation transition so, when you dismiss your view, it just call 'reverse' method from the animationController and you will see the same animation you are using but in reverse.

In case you want to change the animation when you dismiss your view you can do it checking the status of the current animation and compare with AnimationStatus.reverse

This is your code with a Fade animation when it's in reverse.

  class SlideLeftRoute extends PageRouteBuilder {
    final Widget enterWidget;
    SlideLeftRoute({this.enterWidget})
        : super(
            pageBuilder: (BuildContext context, Animation<double> animation,
                Animation<double> secondaryAnimation) {
              return enterWidget;
            },
            transitionsBuilder: (BuildContext context,
                Animation<double> animation,
                Animation<double> secondaryAnimation,
                Widget child) {
              if (animation.status == AnimationStatus.reverse) {
                //do your dismiss animation here
                return FadeTransition(
                  opacity: animation,
                  child: child,
                );
              } else {
                return SlideTransition(
                    position: new Tween<Offset>(
                      begin: const Offset(1.0, 0.0),
                      end: Offset.zero,
                    ).animate(animation),
                    child: child);
              }
            },
          );
  }

WORKAROUND

    class SlideLeftRoute extends PageRouteBuilder {
      final Widget enterWidget;
      final Widget oldWidget;

      SlideLeftRoute({this.enterWidget, this.oldWidget})
          : super(
                transitionDuration: Duration(milliseconds: 600),
                pageBuilder: (BuildContext context, Animation<double> animation,
                    Animation<double> secondaryAnimation) {
                  return enterWidget;
                },
                transitionsBuilder: (BuildContext context,
                    Animation<double> animation,
                    Animation<double> secondaryAnimation,
                    Widget child) {
                  return Stack(
                    children: <Widget>[
                      SlideTransition(
                          position: new Tween<Offset>(
                            begin: const Offset(0.0, 0.0),
                            end: const Offset(-1.0, 0.0),
                          ).animate(animation),
                          child: oldWidget),
                      SlideTransition(
                          position: new Tween<Offset>(
                            begin: const Offset(1.0, 0.0),
                            end: Offset.zero,
                          ).animate(animation),
                          child: enterWidget)
                    ],
                  );
                });
    }

Usage:

 Navigator.of(context)
              .push(SlideLeftRoute(enterWidget: Page2(), oldWidget: this));
diegoveloper
  • 93,875
  • 20
  • 236
  • 194
  • Do you want to say that, if we are running FadeTransition to switch to a new route, then previous route will also run this transition but in reverse order? – CopsOnRoad Oct 11 '18 at 16:46
  • @CopsOnRoad , you could try removing the slidetransition and the animation.status condition – diegoveloper Oct 11 '18 at 16:50
  • Actually your code looks good although I haven't tried it yet. As you wrote in first paragraph that previous route (screen) will run the transition in reverse order automatically (by default). But when I was doing it, the old route was still and only the new route was getting animated. – CopsOnRoad Oct 11 '18 at 16:55
  • 1
    @CopsOnRoad exactly.. what i was trying to say is that the previous screen don't do any animation when i push a new Screen. only the new screen is having a transition. – Guilherme Sant'Ana Oct 11 '18 at 18:41
  • You can add another param in the SlideLeftRoute and pass the old child :) – diegoveloper Oct 11 '18 at 18:43
  • @diegoveloper is that what i'm trying to do, but i'm not able to understand the right way of doing that.. the transitionsBuilder brings only the new Screen as param. it is kind of confusing. – Guilherme Sant'Ana Oct 11 '18 at 18:52
  • Could you add a gif or link video on your question about what you need ? I could help you – diegoveloper Oct 11 '18 at 18:53
  • @diegoveloper Even your solution won't work because the animation status goes from dismissed to forward to complete. There will be no reverse status while animating. – CopsOnRoad Oct 11 '18 at 19:01
  • it only works for the new page 'forward' when enter and 'reverse' when dismiss. – diegoveloper Oct 11 '18 at 19:02
  • I think what OP is saying is "I want to animate the old screen too when I am animating the new screen". Your solution is for the situation "When you go to new screen you have ABC animation and when you come back to previous page you would see reverse of ABC animation" – CopsOnRoad Oct 11 '18 at 19:04
  • it says "Transition Exit" , when you open a new page you have an enter transition, when you dismiss the page you have the exit transition . I know the question was updated, he is looking for an animation for the old view. – diegoveloper Oct 11 '18 at 19:06
  • I found this https://stackoverflow.com/questions/48138595/animate-route-that-is-going-out-being-replaced but it's not very clear, the secondaryAnimation always has 0.0 of value. So if he wants to add a dismiss animation, before you push the view you want animate when dismiss, use the code above, then you will have a dismiss animation ;) – diegoveloper Oct 11 '18 at 19:21
  • @diegoveloper i added a git as example. Note that the screen1 is animating as well. – Guilherme Sant'Ana Oct 11 '18 at 19:53
  • I use a workaround to simulate the effect that you want, check my response updated, anyways I hope someone find a better way to do this. – diegoveloper Oct 11 '18 at 20:06
  • @diegoveloper what if my exit page can be 2 differents animations, por example slide down if scroll and fade on click close button ... how do handle 2 animations over same exit page ??? – Alberto Acuña Jan 04 '21 at 15:45
1

@janosch's answer worked the best for me (so be sure to upvote him if this works for you), but @bihire boris's point about not being able to have more than 2 page routes in a navigation stack was true.

What worked for me was this:

class SlidingPageRouteBuilder extends PageRouteBuilder {
  SlidingPageRouteBuilder({
    required RoutePageBuilder pageBuilder,
  }) : super(
          transitionDuration: const Duration(milliseconds: 300),
          reverseTransitionDuration: const Duration(milliseconds: 300),
          pageBuilder: pageBuilder,
          transitionsBuilder: (BuildContext context,
              Animation<double> animation,
              Animation<double> secondaryAnimation,
              Widget child) {
            final pushingNext =
                secondaryAnimation.status == AnimationStatus.forward;
            final poppingNext =
                secondaryAnimation.status == AnimationStatus.reverse;
            final pushingOrPoppingNext = pushingNext || poppingNext;
            late final Tween<Offset> offsetTween = pushingOrPoppingNext
                ? Tween<Offset>(
                    begin: const Offset(0.0, 0.0), end: const Offset(-1.0, 0.0))
                : Tween<Offset>(
                    begin: const Offset(1.0, 0.0), end: const Offset(0.0, 0.0));
            late final Animation<Offset> slidingAnimation = pushingOrPoppingNext
                ? offsetTween.animate(secondaryAnimation)
                : offsetTween.animate(animation);
            return SlideTransition(position: slidingAnimation, child: child);
          },
        );
}

And the implementation:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: Routes.first,
      onGenerateRoute: Routes.generateRoutes,
      },
    );
  }
}

class Routes {
  static const String first = '/first';
  static const String second = '/second';

  static Route<dynamic>? generateRoutes(RouteSettings settings) {
    switch (settings.name) {
      case first:
        return SlidingPageRouteBuilder(
          pageBuilder: (context, animation, secondaryAnimation) =>
              const FirstPage(),
        );
      case second:
        return SlidingPageRouteBuilder(
          pageBuilder: (BuildContext context, Animation<double> animation,
                  Animation<double> secondaryAnimation) =>
              const SecondPage(),
        );
      default:
        return null;
    }
  }
}

Result:

result

The one downside to this method (and all other methods I've seen) is that the user cannot drag their finger along the left edge of the screen towards the right to pop, in the way that a MaterialPageRoute lets you.

David Chopin
  • 2,780
  • 2
  • 19
  • 40
-1

There is another way to do it. The problem of initState() getting called in oldWidget won't be there anymore.

void main() => runApp(MaterialApp(theme: ThemeData.dark(), home: HomePage()));

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Page 1")),
      body: RaisedButton(
        child: Text("Next"),
        onPressed: () {
          Navigator.push(
            context,
            PageRouteBuilder(
              pageBuilder: (c, a1, a2) => Page2(),
              transitionsBuilder: (context, anim1, anim2, child) {
                return SlideTransition(
                  position: Tween<Offset>(end: Offset(0, 0), begin: Offset(1, 0)).animate(anim1),
                  child: Page2(),
                );
              },
            ),
          );
        },
      ),
    );
  }
}

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Page 2")),
      body: RaisedButton(
        child: Text("Back"),
        onPressed: () => Navigator.pop(context),
      ),
    );
  }
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440