In Flutter, transitions play a vital role in enhancing user experience by providing smooth and visually appealing animations when navigating between different screens. While Flutter offers default transitions, implementing custom transitions allows you to create a unique and branded navigation experience. This article explores how to implement custom transitions between different screens in your Flutter app.
Why Custom Transitions?
Custom transitions provide several benefits:
- Branding: Align transitions with your brand’s visual identity.
- Improved UX: Create more engaging and intuitive navigation.
- Differentiation: Stand out by offering unique and memorable interactions.
Methods for Implementing Custom Transitions
There are several ways to implement custom transitions in Flutter, including using PageRouteBuilder, Hero widgets, and animation controllers.
Method 1: Using PageRouteBuilder
PageRouteBuilder allows you to define custom transitions using animations and routing behaviors. This is one of the most flexible and commonly used approaches.
Step 1: Define Your Custom Transition
Create a function that returns a PageRouteBuilder with your custom animation.
import 'package:flutter/material.dart';
Route createRoute(Widget page) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => page,
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));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
}
In this example:
pageBuilderdefines the widget to be displayed in the route.transitionsBuilderdefines the animation for the transition. This example uses aSlideTransitionthat slides the new page in from the right.
Step 2: Use the Custom Route
Use the custom route when navigating to a new screen.
Navigator.of(context).push(createRoute(const SecondScreen()));
Complete Example:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Custom Transition Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const FirstScreen(),
);
}
}
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('First Screen'),
),
body: Center(
child: ElevatedButton(
child: const Text('Go to Second Screen'),
onPressed: () {
Navigator.of(context).push(createRoute(const SecondScreen()));
},
),
),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Screen'),
),
body: const Center(
child: Text('This is the Second Screen'),
),
);
}
}
Route createRoute(Widget page) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => page,
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));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
}
Method 2: Using Hero Widgets
The Hero widget allows you to create “hero” animations between screens. This is especially useful when transitioning a specific widget from one screen to another.
Step 1: Define Hero Widgets in Both Screens
Wrap the widgets you want to animate with the Hero widget, giving them the same tag.
import 'package:flutter/material.dart';
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('First Screen'),
),
body: Center(
child: Hero(
tag: 'hero-image',
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const SecondScreen()),
);
},
child: Image.network(
'https://via.placeholder.com/150',
),
),
),
),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Screen'),
),
body: Center(
child: Hero(
tag: 'hero-image',
child: Image.network(
'https://via.placeholder.com/300',
width: 300,
height: 300,
),
),
),
);
}
}
In this example:
- Both screens contain a
Herowidget with the sametag(‘hero-image’). - When navigating from the first screen to the second, Flutter automatically animates the widget’s transition between the two screens.
Step 2: Navigate Between Screens
Navigate to the new screen using MaterialPageRoute.
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const SecondScreen()),
);
Complete Example:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hero Transition Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const FirstScreen(),
);
}
}
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('First Screen'),
),
body: Center(
child: Hero(
tag: 'hero-image',
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const SecondScreen()),
);
},
child: Image.network(
'https://via.placeholder.com/150',
),
),
),
),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Screen'),
),
body: Center(
child: Hero(
tag: 'hero-image',
child: Image.network(
'https://via.placeholder.com/300',
width: 300,
height: 300,
),
),
),
);
}
}
Method 3: Using AnimationController
For more complex animations, use AnimationController to create custom animations that you control manually.
Step 1: Set Up the Animation Controller
Initialize the animation controller and define your animation.
import 'package:flutter/material.dart';
class AnimatedScreen extends StatefulWidget {
const AnimatedScreen({super.key});
@override
_AnimatedScreenState createState() => _AnimatedScreenState();
}
class _AnimatedScreenState extends State
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation _offsetAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_offsetAnimation = Tween(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Animated Screen'),
),
body: SlideTransition(
position: _offsetAnimation,
child: const Center(
child: Text('This screen is animated!'),
),
),
);
}
}
In this example:
- An
AnimationControlleris created to manage the animation’s timeline. - An
Animation<Offset>is defined to animate the position of the screen. - The animation starts automatically in
initStateby calling_controller.forward().
Step 2: Implement the Animation
Use the AnimatedScreen in your navigation.
ElevatedButton(
child: const Text('Go to Animated Screen'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const AnimatedScreen()),
);
},
)
Complete Example:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AnimationController Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const FirstScreen(),
);
}
}
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('First Screen'),
),
body: Center(
child: ElevatedButton(
child: const Text('Go to Animated Screen'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const AnimatedScreen()),
);
},
),
),
);
}
}
class AnimatedScreen extends StatefulWidget {
const AnimatedScreen({super.key});
@override
_AnimatedScreenState createState() => _AnimatedScreenState();
}
class _AnimatedScreenState extends State
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation _offsetAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_offsetAnimation = Tween(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Animated Screen'),
),
body: SlideTransition(
position: _offsetAnimation,
child: const Center(
child: Text('This screen is animated!'),
),
),
);
}
}
Tips for Effective Custom Transitions
- Keep it Smooth: Aim for 60 FPS to ensure transitions feel fluid.
- Stay Consistent: Use consistent animations across your app to avoid jarring experiences.
- Test on Different Devices: Ensure animations perform well on a variety of devices.
- Consider Performance: Avoid complex calculations within animation loops to prevent performance bottlenecks.
Conclusion
Implementing custom transitions in Flutter is an excellent way to enhance the visual appeal and user experience of your app. By using PageRouteBuilder, Hero widgets, or AnimationController, you can create unique and engaging navigation animations that set your app apart. Always ensure animations are smooth, consistent, and optimized for performance across different devices.