Flutter Custom Hero Animations with Curves
Flutter's Hero widget provides a beautiful "shared element" transition between routes, giving your app a polished, native feel. By default, Hero animations use a standard interpolation curve, which is often sufficient. However, for unique UI designs or to emphasize specific interactions, you might want to customize this animation. This article will guide you through creating custom Hero animations, specifically focusing on applying a custom curve to the shared element transition.
Understanding Hero Animations
A Hero animation occurs when two Hero widgets, one on the source route and one on the destination route, share the same tag. Flutter automatically animates the widget from its position and size on the source route to its position and size on the destination route. This creates a visually appealing motion that helps users understand the flow of information between screens.
The Default Hero Animation
Let's start with a basic Hero animation example to illustrate the default behavior. Ensure you have the following file structure for your project:
lib/main.dartlib/first_screen.dartlib/second_screen.dart
First, define your main application structure in lib/main.dart:
import 'package:flutter/material.dart';
import 'package:flutter_app/first_screen.dart'; // Assuming your project name is flutter_app
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hero Animation Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const FirstScreen(),
);
}
}
Next, create the FirstScreen in lib/first_screen.dart:
import 'package:flutter/material.dart';
import 'package:flutter_app/second_screen.dart'; // Assuming your project name is flutter_app
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: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Hero(
tag: 'imageHero',
child: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return const SecondScreen();
}));
},
child: Image.network(
'https://picsum.photos/100', // Small image
width: 100.0,
height: 100.0,
),
),
),
const SizedBox(height: 20),
const Text('Tap the image to see Hero animation'),
],
),
),
);
}
}
And finally, the SecondScreen in lib/second_screen.dart:
import 'package:flutter/material.dart';
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: 'imageHero',
child: Image.network(
'https://picsum.photos/400', // Larger image
width: 300.0,
height: 300.0,
),
),
),
);
}
}
Run this code, and you'll observe the image animating smoothly between screens using the default curve, which is typically Curves.fastOutSlowIn.
Customizing Hero Animations with Curves
To introduce a custom curve to our Hero animation, we need to intercept the route transition. Flutter provides the PageRouteBuilder class, which allows us to define custom transitions. While the createRectTween method within PageRouteBuilder is used to define the actual path (e.g., straight or arc) the Hero takes, the speed or timing along that path is controlled by the route's primary animation curve.
We'll create a custom route that applies a specific curve to the route's animation, and since the Hero animation is driven by this route's animation, it will inherit the custom curve. Let's use Curves.elasticOut for a more dramatic, bouncing effect.
Implementing a Custom Hero Route with a Curve
Create a new file lib/custom_hero_route.dart:
import 'package:flutter/material.dart';
class CustomHeroRoute extends PageRouteBuilder {
final Widget page;
final Curve curve;
CustomHeroRoute({required this.page, this.curve = Curves.fastOutSlowIn})
: super(
pageBuilder: (
BuildContext context,
Animation animation,
Animation secondaryAnimation,
) =>
page,
// Define a transition for the route itself. This can be anything.
transitionsBuilder: (
BuildContext context,
Animation animation,
Animation secondaryAnimation,
Widget child,
) =>
FadeTransition( // Example: a simple fade transition for the entire route
opacity: animation,
child: child,
),
transitionDuration: const Duration(milliseconds: 700), // Adjust duration as needed
);
@override
Animation createAnimation() {
// This method is key: It allows us to apply a custom curve to the route's
// primary animation, which in turn drives the Hero transition.
return CurvedAnimation(
parent: super.createAnimation(),
curve: curve,
);
}
// Optionally, you can override createRectTween for a custom Hero *path*.
// For simply applying a curve, the default MaterialRectCenterArcTween path is usually fine.
@override
RectTween createRectTween(Rect? begin, Rect? end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
}
}
Now, modify your FirstScreen (lib/first_screen.dart) to use CustomHeroRoute when navigating to SecondScreen:
import 'package:flutter/material.dart';
import 'package:flutter_app/custom_hero_route.dart'; // Import your custom route
import 'package:flutter_app/second_screen.dart'; // Assuming your project name is flutter_app
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: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Hero(
tag: 'imageHero',
child: GestureDetector(
onTap: () {
Navigator.push(
context,
CustomHeroRoute(
page: const SecondScreen(),
curve: Curves.elasticOut, // Apply your desired curve here!
),
);
},
child: Image.network(
'https://picsum.photos/100',
width: 100.0,
height: 100.0,
),
),
),
const SizedBox(height: 20),
const Text('Tap the image to see Custom Hero animation'),
],
),
),
);
}
}
By simply replacing MaterialPageRoute with CustomHeroRoute and specifying curve: Curves.elasticOut, your Hero animation will now exhibit an elastic, bouncing movement during its transition, thanks to the custom curve applied to the route's primary animation controller.
Conclusion
Flutter's Hero animations are powerful out of the box, but their customizability allows for even richer user experiences. By leveraging PageRouteBuilder and specifically overriding the animation controller's curve via createAnimation, you can dictate the timing and feel of your shared element transitions. This technique enables you to integrate unique motion designs that align perfectly with your application's brand and interaction patterns, making your Flutter apps truly stand out.