On February 21, 2020, the Flutter team launched the animations package as part of Google’s newly announced Material Design motion system. This package lets Flutter developers implement commonly used animations and transition patterns in their apps much more easily than before. In this post, we’ll go over the motion system and show how to use the animations package to create those new transitions in Flutter. We also see how they fit into existing usages of the Navigator and Hero widgets.
An overview of the Material Design motion system
Material Design is a platform-agnostic design system from Google that comes with native iOS, Android, web, and Flutter implementations. The motion system is a new addition to Material Design and consists of four transition patterns that help users understand and navigate through an app:
Nearly every app uses the master-detail pattern, such as navigating from of a list view to a detail view. This is where the Container transform is well suited. By seamlessly transforming a list tile into a detail page, the relationship between the two components is maintained. As long as there is an enclosed area that acts as a persistent element in the transition, it is a good candidate for the Container transform. Other examples are the transition of a search bar into an expanded search page with more options and filters, and a Chip into an expanded chip. The Container transform does not strictly have to be full-screen; it can also apply in scenarios where a smaller component expands into a larger one.
The Shared Axis pattern can be used if the elements in a transition do not have a persistent container, but still share a common navigational or horizontal/vertical layout. A good example is the forward and backward navigation in a typical onboarding flow. In this case, the Shared Axis pattern adds a horizontal translation plus a fade-in and fade-out to each page transition. The translation is not limited to the x-axis; it can be applied to the y-axis (vertical), or z-axis (depth) as well.
The Fade through pattern applies to scenarios where UI components do not necessarily have a strong spatial or navigational relationship with each other. This pattern replaces content in-place with a combination fade and scale transition. This is useful when there is a substantial change to the content being displayed. For example, when tapping on a refresh button to reload data on a screen. When the UI updates, the fade and scale effect gives the user the impression that the data was changed substantially.
The Fade transition applies to scenarios where a UI element simply enters or exits the screen, unlike the Fade through transition where the UI elements appear to update in-place. This is also accomplished using a fade and scale animation, but can also be repositioned to appear to emerge from a particular part of the screen. By default, the transition scales from the center. A good use case for this transition is the presentation of a dialog or drop-down menu that would be jarring if it appeared immediately.
The Flutter animations package
As a first-class citizen in Material Design, we are pleased that Flutter had support for the new motion system on day 1 via the animations package! This is a first-party package from the Flutter team and is available on pub.dev. Let’s dive into some code and figure out how to use it.
The new Material Design Container transform is implemented in Flutter using the OpenContainer widget. It transitions between two child widgets seamlessly so that they appear to be the same widget. The children are provided via the closedBuilder and openBuilder properties. These builder functions provide a standard BuildContext as well as an additional action callback, which is used to trigger the animation to open and close the widget. There is also a tappable parameter (which defaults to true) which can trigger the “open” transition on tap as an alternative to the action callback. Note that it does not also trigger the “closed” animation.
We can specify the color, elevation, and shape of the closed and open states, and Flutter will seamlessly interpolate between them. You can alter the transition duration as well as the transition type. The ContainerTransitionType.fade value overlaps the widgets in a cross fade; the ContainerTransitionType.fadeTrough value staggers the widget fades so that they do not overlap.
It is important to note that the OpenContainer widget must be used in a context with a Navigator. When it transitions from the closed widget to the open widget, it pushes a new PageRoute onto its closest ancestor Navigator. It then grows the open widget to fill the entire size of this Navigator. Most Navigators are full-screen, but don’t have to be. Here is an example that generates the GIF above:
The new Material Design Shared Axis transition is implemented in Flutter using the SharedAxisTransition widget. It generates a shared translation + fade transition in either the x, y, or z axis. It accepts a child widget as a parameter. The animation between the “old” and “new” child widgets is controlled by the animation and secondaryAnimation parameters. This means that the SharedAxisTransition widget can’t live on its own and must be “driven” by an outside source. One such source is the new PageTransitionSwitcher widget, which is part of the animations package and is used to align transitions between widgets. This widget is similar to the AnimatedSwitcher widget, which does a cross-fade between it’s “old” and “new” child, but lets us define the enter and exit animations separately.
To use the SharedAxisTransition transition by default for all routes inside a Navigator, you can add a PageTransitionsTheme to the ThemeData of MaterialApp. Add a SharedAxisPageTransitionsBuilder to the builders property with the desired transition type for every platform where you want this transition to apply:
Be careful though — this technique is an all-or-none approach, and it only works for named routes. But what if we want to push routes programmatically onto a Navigator, as in the following example?
final route = MaterialPageRoute(builder: (_) => MyWidget());
This technique helps us better decouple our content (MyWidget) from our transition (MaterialPageRoute). To use the SharedAxisTransition transition, we can just create a new PageRoute that consumes it. We can either subclass PageRoute or use a PageRouteBuilder:
Then we can continue to push new screens on our Navigator programmatically:
The new Material Design Fade through transition is implemented in Flutter using the FadeThroughTransition widget. This widget applies a fade transition to the outgoing element and a fade and scale transition to the incoming element. Similar to the SharedAxisTransition, it can also be used inside a PageTransitionSwitcher widget, applied globally to all named Navigator routes, or applied selectively to only certain routes via a custom PageRoute.
The new Material Design Fade transition is implemented in Flutter using the FadeScaleTransition widget. Entering elements fade in with increasing opacity and scale from 80% to 100%, while exiting elements fade out with decreasing opacity. It can also be used inside a PageTransitionSwitcher widget, applied globally to all named Navigator routes, applied selectively using a custom PageRoute, or shown using the new showModal() method:
The Flutter animations package introduces a new global function called showModal() which can be used to display content in a modal in the current Navigator. This is very similar to the existing showGeneralDialog() function which is part of material.dart. The difference is that the modal configuration properties have been pulled out into a single configuration argument of type ModalConfiguration. This class contains various characteristics of the modal, such as the enter and exit transitions, the duration of the transitions, and modal barrier properties. Additionally, there is a subclass called FadeScaleTransitionConfiguration which applies the standard values from the new Material Design Fade transition. Here is an example of how to use it in conjunction with the showModal() method:
A note about Hero widgets
The Hero widget can be used to create a pleasant visual effect when transitioning between routes in a Navigator. A Hero widget appears to from its original location in the old route to its final location in the new route. However, in our testing, Hero widgets do not work with the new Material Design transitions when used inside a OpenContainer or a PageTransitionSwitcher. However, they do work when using the PageRoute subclassing technique shown above.
The new motion system is an exciting evolution of Material Design and reflects the increasing importance of motion effects in modern application design. The new transitions are subtle and communicative without being overbearing. It is fantastic that they have first-class support in Flutter via the animations package. We are especially excited that they can also be used as custom PageRoute implementations. We encourage you to give it a try!
Disclaimer: The observations above are the result of our quick testing of the new Flutter animations package. Let us know in the comments if you find anything different in your own testing.
Kawyn Furstoss and Hashim Hayat contributed to this blog post.