Flutter (64): Custom route switching animation

Time: Column:Mobile & Frontend views:240

Route Management and Custom Route Transition Animations

In the "Route Management" section, we mentioned that the Material component library provides a MaterialPageRoute component, which uses platform-specific route transition animations. For example, on iOS, routes transition with a left-right slide, while on Android, they slide vertically. But what if we want to use the left-right transition on Android as well? A simple solution is to use the CupertinoPageRoute directly, like this:

Navigator.push(
  context,
  CupertinoPageRoute(
    builder: (context) => PageB(),
  ),
);

CupertinoPageRoute is a route transition component provided by the Cupertino component library, implementing the left-right sliding transition commonly seen in iOS. But how do we create custom route transition animations? The answer is PageRouteBuilder. Below, we will explore how to use PageRouteBuilder to customize route transition animations. For example, if we want to implement a fade-in, fade-out transition, we can do so with the following code:

Navigator.push(
  context,
  PageRouteBuilder(
    transitionDuration: Duration(milliseconds: 500), // 500ms transition duration
    pageBuilder: (BuildContext context, Animation animation,
        Animation secondaryAnimation) {
      return FadeTransition(
        // Use fade-in, fade-out transition
        opacity: animation,
        child: PageB(), // Route B
      );
    },
  ),
);

As you can see, the pageBuilder function receives an animation parameter provided by Flutter's route manager. During the route transition, pageBuilder is called on every animation frame, allowing us to define custom transition animations using the animation object.

Both MaterialPageRoute, CupertinoPageRoute, and PageRouteBuilder inherit from the PageRoute class. In fact, PageRouteBuilder is just a wrapper around PageRoute. We can directly inherit from the PageRoute class to create custom routes. The example above can also be implemented by defining a custom route class like this:

Defining a Custom FadeRoute Class

class FadeRoute extends PageRoute {
  FadeRoute({
    required this.builder,
    this.transitionDuration = const Duration(milliseconds: 300),
    this.opaque = true,
    this.barrierDismissible = false,
    this.barrierColor,
    this.barrierLabel,
    this.maintainState = true,
  });

  final WidgetBuilder builder;

  @override
  final Duration transitionDuration;

  @override
  final bool opaque;

  @override
  final bool barrierDismissible;

  @override
  final Color barrierColor;

  @override
  final String barrierLabel;

  @override
  final bool maintainState;

  @override
  Widget buildPage(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation) => builder(context);

  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    return FadeTransition(
      opacity: animation,
      child: builder(context),
    );
  }
}

Using FadeRoute

Navigator.push(context, FadeRoute(builder: (context) {
  return PageB();
}));

While both approaches can implement custom route animations, it is recommended to use PageRouteBuilder when possible, as it avoids the need to define a new route class, making it easier to use. However, there are situations where PageRouteBuilder may not be sufficient. For example, when applying transition animations, you may need to access certain properties of the current route. In such cases, inheriting from PageRoute is the only option.

For instance, if we want to apply the animation only when opening a new route, but not when returning, we would need to check the isActive property of the current route when building the transition animation. The code would look like this:

@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
  // The current route is active, meaning a new route is being opened
  if (isActive) {
    return FadeTransition(
      opacity: animation,
      child: builder(context),
    );
  } else {
    // Returning, so no transition animation is applied
    return Padding(padding: EdgeInsets.zero);
  }
}

For more details on route parameters, you can refer to the API documentation. The information is fairly straightforward, so we won't go into further details here.