Implementing Custom Route Transitions to Add Visual Flair to Navigation in Flutter

Flutter provides a robust navigation system that allows developers to move between different screens or routes within an application. While the default route transitions are functional, they often lack visual flair and can feel generic. Implementing custom route transitions can significantly enhance the user experience by providing more engaging and visually appealing animations during navigation.

Understanding Route Transitions in Flutter

A route transition in Flutter is the animation that occurs when navigating from one screen (route) to another. By default, Flutter provides transitions like sliding in from the right (for MaterialPageRoute on Android) or sliding up from the bottom (on iOS). However, Flutter allows you to customize these transitions to create a unique visual experience.

Why Use Custom Route Transitions?

  • Enhanced User Experience: Engaging animations make the navigation feel more fluid and interactive.
  • Brand Consistency: Custom transitions can align with your application’s branding and design language.
  • Unique Visual Effects: Distinguish your app by providing transitions that are not commonly seen.

How to Implement Custom Route Transitions in Flutter

Implementing custom route transitions involves creating a custom PageRouteBuilder or using the PageRoute directly with custom animation code. Below are several approaches to achieving this.

Method 1: Using PageRouteBuilder

PageRouteBuilder is a versatile class that allows you to define custom page routes with custom transitions.

Step 1: Create a Custom PageRouteBuilder

Here’s how to create a PageRouteBuilder with a custom fade transition:


import 'package:flutter/material.dart';

class FadePageRoute extends PageRouteBuilder {
  final Widget child;

  FadePageRoute({required this.child})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => child,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return FadeTransition(opacity: animation, child: child);
          },
        );
}

Explanation:

  • pageBuilder: Defines the widget to be displayed on the new route.
  • transitionsBuilder: Defines the transition animation. In this case, a simple fade transition using FadeTransition.
Step 2: Use the Custom Route in Navigation

Now, use this custom route when navigating:


Navigator.of(context).push(
  FadePageRoute(child: SecondScreen()),
);

Here’s a complete example incorporating this into a simple app:


import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Route Transitions',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: FirstScreen(),
    );
  }
}

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Go to Second Screen with Fade Transition'),
          onPressed: () {
            Navigator.of(context).push(
              FadePageRoute(child: SecondScreen()),
            );
          },
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Screen'),
      ),
      body: Center(
        child: Text('This is the second screen with a fade transition.'),
      ),
    );
  }
}

class FadePageRoute extends PageRouteBuilder {
  final Widget child;

  FadePageRoute({required this.child})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => child,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return FadeTransition(opacity: animation, child: child);
          },
        );
}

Method 2: Custom Slide Transition

Implementing a slide transition involves using an Animation to control the position of the new screen as it slides in.

Step 1: Create a Custom Slide Route

class SlidePageRoute extends PageRouteBuilder {
  final Widget child;
  final AxisDirection direction;

  SlidePageRoute({
    required this.child,
    this.direction = AxisDirection.right,
  }) : super(
          pageBuilder: (context, animation, secondaryAnimation) => child,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            const begin = Offset(1.0, 0.0);
            const end = Offset.zero;
            const curve = Curves.ease;

            var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
            var offsetAnimation = animation.drive(tween);

            return SlideTransition(
              position: offsetAnimation,
              child: child,
            );
          },
        );
}

Explanation:

  • begin: Defines the starting position of the screen. (1.0, 0.0) means the screen starts off to the right.
  • end: Defines the ending position of the screen. Offset.zero means the screen ends at its normal position.
  • Curve: Specifies the animation curve (e.g., ease, linear).
  • SlideTransition: Applies the offset animation to slide the screen into view.
Step 2: Use the Custom Slide Route

Navigator.of(context).push(
  SlidePageRoute(child: SecondScreen(), direction: AxisDirection.left),
);

Complete example:


import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Route Transitions',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: FirstScreen(),
    );
  }
}

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Go to Second Screen with Slide Transition'),
          onPressed: () {
            Navigator.of(context).push(
              SlidePageRoute(child: SecondScreen(), direction: AxisDirection.left),
            );
          },
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Screen'),
      ),
      body: Center(
        child: Text('This is the second screen with a slide transition.'),
      ),
    );
  }
}

class SlidePageRoute extends PageRouteBuilder {
  final Widget child;
  final AxisDirection direction;

  SlidePageRoute({
    required this.child,
    this.direction = AxisDirection.right,
  }) : super(
          pageBuilder: (context, animation, secondaryAnimation) => child,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            const begin = Offset(1.0, 0.0);
            const end = Offset.zero;
            const curve = Curves.ease;

            var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
            var offsetAnimation = animation.drive(tween);

            return SlideTransition(
              position: offsetAnimation,
              child: child,
            );
          },
        );
}

Method 3: Hero Animations

Hero animations are great for transitioning shared elements between routes, providing a smooth and contextual transition.

Step 1: Wrap the Shared Widget with Hero

On both the source and destination screens, wrap the shared widget with a Hero widget, providing the same tag.

First Screen:


Hero(
  tag: 'shared-image',
  child: Image.asset('assets/image1.jpg', width: 100, height: 100),
)

Second Screen:


Hero(
  tag: 'shared-image',
  child: Image.asset('assets/image1.jpg', width: 200, height: 200),
)
Step 2: Implement Navigation

Simply navigate to the new route, and Flutter will automatically handle the hero animation.


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

Complete example:


import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hero Animation Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: FirstScreen(),
    );
  }
}

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: InkWell(
          onTap: () {
            Navigator.of(context).push(
              MaterialPageRoute(builder: (context) => SecondScreen()),
            );
          },
          child: Hero(
            tag: 'shared-image',
            child: Image.network('https://via.placeholder.com/100', width: 100, height: 100),
          ),
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Screen'),
      ),
      body: Center(
        child: Hero(
          tag: 'shared-image',
          child: Image.network('https://via.placeholder.com/200', width: 200, height: 200),
        ),
      ),
    );
  }
}

Method 4: Using Third-Party Packages

Several Flutter packages offer pre-built custom route transitions and animations. One popular package is animations.

Step 1: Add animations Package

Add the animations package to your pubspec.yaml file:


dependencies:
  flutter:
    sdk: flutter
  animations: ^2.0.0
Step 2: Use Pre-built Transitions

import 'package:animations/animations.dart';
import 'package:flutter/material.dart';

Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => SecondScreen(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return SharedAxisTransition(
        animation: animation,
        secondaryAnimation: secondaryAnimation,
        transitionType: SharedAxisTransitionType.horizontal,
        child: child,
      );
    },
  ),
);

Complete example:


import 'package:flutter/material.dart';
import 'package:animations/animations.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Animations Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: FirstScreen(),
    );
  }
}

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          child: Text('Go to Second Screen with SharedAxisTransition'),
          onPressed: () {
            Navigator.of(context).push(
              PageRouteBuilder(
                pageBuilder: (context, animation, secondaryAnimation) => SecondScreen(),
                transitionsBuilder: (context, animation, secondaryAnimation, child) {
                  return SharedAxisTransition(
                    animation: animation,
                    secondaryAnimation: secondaryAnimation,
                    transitionType: SharedAxisTransitionType.horizontal,
                    child: child,
                  );
                },
              ),
            );
          },
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Screen'),
      ),
      body: Center(
        child: Text('This is the second screen with a SharedAxisTransition.'),
      ),
    );
  }
}

Best Practices for Custom Route Transitions

  • Keep Transitions Consistent: Maintain a consistent style of transitions throughout the app to avoid jarring the user.
  • Performance: Ensure the transitions are optimized for performance to avoid janky animations.
  • Accessibility: Provide alternatives or disable animations for users who prefer reduced motion.
  • Testing: Test the transitions on different devices and screen sizes to ensure they look good everywhere.

Conclusion

Implementing custom route transitions can significantly elevate the user experience in Flutter applications. Whether using PageRouteBuilder, custom animations, Hero animations, or third-party packages, the ability to tailor these transitions to your application’s brand and design will set your app apart. By following best practices, you can ensure these transitions are both visually appealing and performant, providing a seamless navigation experience for your users.