0  comments

Navigating between routes is quite bland by default. Flutter graciously provides you with the MaterialPageRoute and CupertinoPageRoute classes and, while their transition animations don't look bad, there's certainly something more we can do.

Could we, for example, animate parts of the pushed page independently and even make the animation staggered? Of course we can! We're in Flutter, after all πŸ’™

The project we're going to build

By the end of this tutorial, you are going to know how to build a fully custom page transition consisting of translating two Containers. You can then take this approach and apply it to all kinds of crazy effects - anything is possible with the Transform widget.

Exploring the default transition

The starter project contains two pages. After pressing the floating action button in the FirstPage, the following code is executed.

first_page.dart

Navigator.of(context).push(
  MaterialPageRoute(
    builder: (context) => SecondPage(),
  ),
);

We're then taken to the SecondPage while seeing an uninspiring "full page" slide transition. This page contains two simple Containers inside a Column.

second_page.dart

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Expanded(
            child: Container(
              color: Colors.red,
            ),
          ),
          Expanded(
            child: Container(
              color: Colors.green,
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          Navigator.of(context).pop();
        },
        label: Text('Navigate Back'),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
    );
  }
}

The FloatingActionButton held inside a Scaffold is animated for free as a Hero widget. While that livens up the transition quite a bit, we want to go more extreme. Let's animate the Containers.

Goodbye, MaterialPageRoute πŸ‘‹

Fully custom page transition is not possible with the MaterialPageRoute because it hides a whole lot of objects from us. In order to get hold of the transition Animation object, we need to use a PageRouteBuilder.

Animation in Flutter is, in essence, only a value smoothly transitioning between its start and end boundaries. You can brush up on animations in this tutorial.

As with any animation, even page transitions can be configured to have a certain duration. By default, it takes 300 milliseconds but we're going to set the duration to a full second. Lastly, we want to pass the animation parameter of the pageBuilder method to the SecondPage.

first_page.dart

Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return SecondPage(
        transitionAnimation: animation,
      );
    },
    transitionDuration: Duration(seconds: 1),
  ),
);
Consider using named routes in production-grade projects. This animation approach will still work the same if you use PageRouteBuilders inside the onGenerateRoute method.

Slide animation

Now we need to add the transitionAnimation field into the SecondPage class and use it!

The most versatile choice would be to use the Transform widget, in this case, the simplified Transform.translate constructor. With it, Skia is the limit πŸ™ƒ

Since we're only going to slide the widgets, we may as well use the SlideTransition because it's a bit simpler to use. The transitionAnimation is passed by reference just like any other object in Dart. This means that it will be updated as usual and we can listen to these updates using an AnimatedBuilder widget. Sprinkle in some Tween and you have things moving on the screen!

second_page.dart

Column(
  children: <Widget>[
    // Expanded remains as the direct child of a Column
    Expanded(
      child: AnimatedBuilder(
        animation: transitionAnimation,
        builder: (context, child) {
          return SlideTransition(
            position: Tween<Offset>(
              // X, Y - Origin (0, 0) is in the upper left corner.
              begin: Offset(1, 0),
              end: Offset(0, 0),
            ).animate(transitionAnimation),
            child: child,
          );
        },
        child: Container(
          color: Colors.red,
        ),
      ),
    ),
    Expanded(
      child: AnimatedBuilder(
        animation: transitionAnimation,
        builder: (context, child) {
          return SlideTransition(
            position: Tween<Offset>(
              begin: Offset(-1, 0),
              end: Offset(0, 0),
            ).animate(transitionAnimation),
            child: child,
          );
        },
        child: Container(
          color: Colors.green,
        ),
      ),
    ),
  ],
),

Apart from the horrible code duplication (which we'll address in just a second), there's something else that doesn't play right. After running the app, we see that the animation is not staggered as in the video above.

Staggered Animation

Making the red container arrive sooner than the green one is very simple. When you think about it, both of the animations are linear, going from 0 to 1. 

What we want to do is to change the curve so that the red animation will arrive at the value 1 first and only then start changing the value of the green animation.

This is achievable by using an Interval curve in conjunction with a CurvedAnimation which will be driven by our transitionAnimation. While we're at it, we can also swap the unexciting linear nature of the animation for something more smooth, for example, Curves.easeOutCubic. This is how the red widget will look like:

second_page.dart

Expanded(
  child: AnimatedBuilder(
    animation: transitionAnimation,
    builder: (context, child) {
      return SlideTransition(
        position: Tween<Offset>(
          begin: Offset(1, 0),
          end: Offset(0, 0),
        ).animate(
          CurvedAnimation(
            curve: Interval(0, 0.5, curve: Curves.easeOutCubic),
            parent: transitionAnimation,
          ),
        ),
        child: child,
      );
    },
    child: Container(
      color: Colors.red,
    ),
  ),
),

Changes to the green widget will be almost identical, only the Interval will go from 0.5 to 1.

second_page.dart

// Green widget
CurvedAnimation(
  curve: Interval(0.5, 1, curve: Curves.easeOutCubic),
  parent: transitionAnimation,
),

Removing duplication with a Provider

We have only two widgets and duplication makes the code hard to maintain even now. We must get rid of it and because we don't want to pass the transitionAnimation through a bunch of constructors, we're going to use the provider package. Let's add it as a dependency.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.1.2

Inside the FirstPage, we now want to provide the animation to the SecondPage. Because Animation is a subclass of Listenable, we have to use the ListenableProvider in order not to get any errors although we would be fine with a regular Provider in this case too.

first_page.dart

Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) {
      return ListenableProvider(
        create: (context) => animation,
        child: SecondPage(),
      );
    },
    transitionDuration: Duration(seconds: 1),
  ),
);

We can now extract the duplicated widget (all the way from AnimatedBuilder) from the SecondPage. We'll call it SlidingContainer and make initialOffsetX, intervalStart, intervalEnd, and color configurable. The transition animation will not be passed through any constructor. Instead, it will be gotten directly inside the SlidingContainer widget as highlighted below.

second_page.dart

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Expanded(
            child: SlidingContainer(
              color: Colors.red,
              initialOffsetX: 1,
              intervalStart: 0,
              intervalEnd: 0.5,
            ),
          ),
          Expanded(
            child: SlidingContainer(
              color: Colors.green,
              initialOffsetX: -1,
              intervalStart: 0.5,
              intervalEnd: 1,
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          Navigator.of(context).pop();
        },
        label: Text('Navigate Back'),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
    );
  }
}

class SlidingContainer extends StatelessWidget {
  final double initialOffsetX;
  final double intervalStart;
  final double intervalEnd;
  final Color color;

  const SlidingContainer({
    Key key,
    this.initialOffsetX,
    this.intervalStart,
    this.intervalEnd,
    this.color,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final animation = Provider.of<Animation<double>>(context, listen: false);

    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        return SlideTransition(
          position: Tween<Offset>(
            begin: Offset(initialOffsetX, 0),
            end: Offset(0, 0),
          ).animate(
            CurvedAnimation(
              curve: Interval(
                intervalStart,
                intervalEnd,
                curve: Curves.easeOutCubic,
              ),
              parent: animation,
            ),
          ),
          child: child,
        );
      },
      child: Container(
        color: color,
      ),
    );
  }
}

And there you have it! A fully custom staggered page transition animation in Flutter. Now that you know the principles, make sure to apply them in the most ingenious of ways!

About the author 

Matt ReΕ‘etΓ‘r

Matt is an app developer with a knack for teaching others. Working as a Flutter freelancer and most importantly developer educator, he doesn't have a lot of free time πŸ˜… Yet he still manages to squeeze in tough workouts πŸ’ͺ and guitar 🎸

You may also like

Flutter Firebase & DDD Course [5] – Sign-In Form Logic

Dio Connectivity Retry Interceptor – Flutter Tutorial

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
>