Flutter: The new ‘animations’ package explained

The Flutter team recently dropped a great new transitions package, based on the new Material 2 design spec, the somewhat ambiguously-named: animations package.

SharedAxis Example

They are super cool to look at and appear to be highly performant. The only issue? The examples they’ve provided with the package are pretty hard to follow (coming in at close to 1500 lines!) and there’s no code snippets at all in the README.

But fear not! This package is actually extremely simple to use once you clear away the noise, and can see how it works.

First, lets recap. The package itself is composed of 4 “transition patterns”:

  • SharedAxis – A PageRoute that transitions on the X, Y or Z axis, where Z represents scale. Recommended for pages that are related to one another (login, onboarding etc)
  • FadeThrough – A PageRoute where outgoing elements fade out, then incoming elements fade in and scale up. Recommended for pages that have no strong relationship to eachother.
  • FadeScale– A Modal PageRoute (meant for dialogs). Elements that enter use a quick fade in and scale from 80% to 100%. Elements that exit simply fade out.
  • OpenContainer – A container that grows to fill the screen to reveal new content when tapped. Similar to a Hero widget.

We’ll show you how to use the various page routes first, and then we’ll talk a bit more about using OpenContainer.

They’re all just page transitions…

Using SharedAxis, FadeThrough and FadeScale really could not be easier. They’re just Transitions you can use inside your PageRoutes, so you can just define some static builder methods like so:

static Route<T> fadeThrough<T>(PageBuilder page, [double duration = kDefaultDuration]) {
    return PageRouteBuilder<T>(
      transitionDuration: Duration(milliseconds: (duration * 1000).round()),
      pageBuilder: (context, animation, secondaryAnimation) => page(),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        return FadeThroughTransition(animation: animation, secondaryAnimation: secondaryAnimation, child: child);
      },
    );
  }

  static Route<T> fadeScale<T>(PageBuilder page, [double duration = kDefaultDuration]) {
    return PageRouteBuilder<T>(
      transitionDuration: Duration(milliseconds: (duration * 1000).round()),
      pageBuilder: (context, animation, secondaryAnimation) => page(),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        return FadeScaleTransition(animation: animation, child: child);
      },
    );
  }

  static Route<T> sharedAxis<T>(PageBuilder page, [SharedAxisTransitionType type = SharedAxisTransitionType.scaled, double duration = kDefaultDuration]) {
    return PageRouteBuilder<T>(
      transitionDuration: Duration(milliseconds: (duration * 1000).round()),
      pageBuilder: (context, animation, secondaryAnimation) => page(),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        return SharedAxisTransition(
          child: child,
          animation: animation,
          secondaryAnimation: secondaryAnimation,
          transitionType: type,
        );
      },
    );
  }

Now you can call any of these transition types with the following:

//Show SharedAxis Page - Provides 3 axis, vt, hz and scale
Navigator.push(context, PageRoutes.sharedAxis(()=>MyPage(), SharedAxisTransitionType.scaled));

//Show FadeThrough Page
Navigator.push(context, PageRoutes.fadeThrough(()=>MyPage()));

//Show FadeScale dialog
showModal(
    context: context, 
    configuration: FadeScaleTransitionConfiguration(),
    builder: (BuildContext context) => MyDialog());


That’s really all there is to it for those. They’re just fancy page routes, that you can use like any other! You can read more about the recommended usage of each pattern in the Material Design Specs.

FadeScale
FadeThrough

OpenContainer Widget

You can kind of think about OpenContainer as an easier to use Hero. The main difference is that while a Hero could have any defined region, Container is meant to always have a partial-screen portion, and a full-screen portion. Also, no matching tag is required!

OpenContainer

As you can see, it’s also a little more robust than the Hero. By using a simple cross-fade + scale effect it’s much more forgiving of layout differences between the start and end views.

Where Hero would really have to be massaged to look good, OpenContainer tends to look nice out of the box. It also doesn’t seem to have any of the Text rendering issues that sometimes plague Hero. All in all, a much more practical and easier to use solution, producing a fairly similar effect in the end.

There is really no great trick to using OpenContainer it’s just a simple little widget, with a couple of builders:

OpenContainer(
      transitionDuration: 500.milliseconds,
      closedBuilder: (BuildContext c, VoidCallback action) => Text("Click Me"),
      openBuilder: (BuildContext c, VoidCallback action) => SomeNewPage(),
      tappable: true,
    )

One important thing to note about OpenContainer, is that it does actually use a PageRoute under the hood. This means a simple Navigator.pop() is all you need to close the container when it’s open, and that hardware back button support will work as you’d expect it. *fist pump*

The other thing that needs explaining here is this action() callback. The purpose of these, are to manually open or the close the container.

By default, the OpenContainer will open itself if you have .tappable set to true, and it will close itself, when you Navigator.pop(), but what if you don’t want the entire Widget to be tappable? That’s where the actions come in.

With the action, we can set .tappable to false, and just open things ourselves by calling the action() inside our own button handler:

OpenContainer(
      transitionDuration: 500.milliseconds,
      closedBuilder: (BuildContext c, VoidCallback action) => 
        RaisedButton(child: Text("Open"), 
            onPressed: ()=>action()),
      openBuilder: (BuildContext c, VoidCallback action) => SomeNewPage(),
      tappable: false,
    )

Here you can see that we used a button, to manually call the action, which will open up the OpenContainer. We could also do the same thing inside the openBuilder, but as far as we know, this is the same as just calling Navigator.pop().

Speaking of Navigator.pop, there is a rather large flaw currently in the component. The Future that is normally generated by a Navigator.push() is not made available anywhere. This means that there is no way to await the results from the page that was opened, which is a very common use case in Flutter.

This does limit the potential of this component in a pretty severe way, but it should also be a relatively easy fix, so hopefully the team will add that in. If you agree, you can up-vote the issue here: https://github.com/flutter/flutter/issues/51750

That’s about it! Thanks for checking out the post, and we hope this has given you a good overview of the new animations package and some of its capabilities.

shawn.blais

Shawn has worked as programmer and product designer for over 20 years, shipping several games on Steam and Playstation and dozens of apps on mobile. He has extensive programming experience with Dart, C#, ActionScript, SQL, PHP and Javascript and a deep proficiency with motion graphics and UX design. Shawn is currently the Technical Director for gskinner.

@tree_fortress

8 Comments

  1. Fadhli Sulaimi March 20, 2020 at 11:25pm

    I tried this and it’s awesome! But in iOS, for openContainer I cant seem to swipe left to go back to previous screen. Am i doing it wrongly?

  2. Good question! I’m not sure, we haven’t tested this ourselves. Are the other routes popped with back-swipe?

  3. Fadhli Sulaimi March 21, 2020 at 2:54am

    I have not yet tried all, but I managed to go around this by wrapping GestureDetector on the root Widget. Then i just pop the route on edge left swipe. The only issue about this is, you wont have that manual ‘fancy’ dragging as u swipe.

  4. Thanks!

  5. you can do the same with the EFL libraries (www.enlightenment.org), and has done that since 15 years. Example : http://www.rasterman.com/files/wp2.avi

  6. I really love the detail explain

    Would
    “`
    RoutePageBuilder page
    “`
    Rather than
    “`
    PageBuilder page
    “`
    on Flutter 1.7.1

  7. For the FadeScaleTransition and showModal, does this work with showDatePicker()? I understand its a modal, but it also needs to return a value (the Future) of the date picked.

  8. How can we set duration for each opening and closing seperately? (ex. 600ms opening, 300ms closing)

Leave a Reply

Your email address will not be published. Required fields are marked *