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.