Hero Animation
Smooth transitions between screens
🦸 What is Hero Animation?
Hero animations create seamless transitions when navigating between screens by animating a shared element. The widget "flies" from one screen to another, providing visual continuity and professional polish to your app's navigation experience effortlessly.
// Wrap widget with Hero and matching tag
Hero(
tag: 'imageHero',
child: Image.asset('photo.jpg'),
)
Hero Animation Concepts
Unique Tags
Match widgets across screens
Hero(
tag: 'uniqueId',
child: widget,
)
Automatic Animation
Flutter handles the transition
Navigator.push(
context,
MaterialPageRoute(...),
)
Screen Transitions
Works with any navigation
// Both screens need
// matching Hero tags
Custom Flight
Customize animation behavior
flightShuttleBuilder:
(context, animation, ...) {}
🔹 Basic Hero Animation
Simple hero animation between two screens:
// First Screen (List)
class ListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Photo List')),
body: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(),
),
);
},
child: Hero(
tag: 'imageHero',
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.photo,
size: 50,
color: Colors.white,
),
),
),
),
);
}
}
// Second Screen (Detail)
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Photo Detail')),
body: Center(
child: Hero(
tag: 'imageHero', // Same tag!
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.photo,
size: 150,
color: Colors.white,
),
),
),
),
);
}
}
🔹 Hero with Images
Common use case with image galleries:
// Gallery Screen
class GalleryScreen extends StatelessWidget {
final List images = [
'assets/photo1.jpg',
'assets/photo2.jpg',
'assets/photo3.jpg',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Gallery')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: images.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ImageDetailScreen(
imageUrl: images[index],
tag: 'image$index',
),
),
);
},
child: Hero(
tag: 'image$index',
child: Image.asset(
images[index],
fit: BoxFit.cover,
),
),
);
},
),
);
}
}
// Detail Screen
class ImageDetailScreen extends StatelessWidget {
final String imageUrl;
final String tag;
ImageDetailScreen({required this.imageUrl, required this.tag});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
onTap: () => Navigator.pop(context),
child: Center(
child: Hero(
tag: tag,
child: Image.asset(imageUrl),
),
),
),
);
}
}
🔹 Hero with Custom Child
Animate complex widgets:
// Product List
class ProductCard extends StatelessWidget {
final String productId;
final String name;
final double price;
ProductCard({
required this.productId,
required this.name,
required this.price,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetail(
productId: productId,
name: name,
price: price,
),
),
);
},
child: Card(
child: Column(
children: [
Hero(
tag: 'product-$productId',
child: Container(
width: 150,
height: 150,
color: Colors.orange,
child: Icon(Icons.shopping_bag, size: 60),
),
),
Text(name),
Text('\$$price'),
],
),
),
);
}
}
// Product Detail
class ProductDetail extends StatelessWidget {
final String productId;
final String name;
final double price;
ProductDetail({
required this.productId,
required this.name,
required this.price,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(name)),
body: Column(
children: [
Hero(
tag: 'product-$productId',
child: Container(
width: double.infinity,
height: 300,
color: Colors.orange,
child: Icon(Icons.shopping_bag, size: 150),
),
),
Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
Text(
name,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Text(
'\$$price',
style: TextStyle(fontSize: 32, color: Colors.green),
),
],
),
),
],
),
);
}
}
🔹 Custom Hero Flight
Customize the animation transition:
class CustomHeroFlight extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
},
child: Hero(
tag: 'customHero',
flightShuttleBuilder: (
BuildContext flightContext,
Animation animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
// Custom widget during flight
return RotationTransition(
turns: animation,
child: Material(
color: Colors.transparent,
child: Icon(
Icons.star,
size: 100,
color: Colors.yellow,
),
),
);
},
child: Icon(
Icons.star,
size: 50,
color: Colors.yellow,
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: Hero(
tag: 'customHero',
child: Icon(
Icons.star,
size: 150,
color: Colors.yellow,
),
),
),
);
}
}
🔹 Radial Hero Animation
Create expanding circle transitions:
class RadialHeroAnimation extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ExpandedScreen()),
);
},
child: Hero(
tag: 'radialHero',
createRectTween: (begin, end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
},
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.purple,
shape: BoxShape.circle,
),
child: Icon(Icons.add, color: Colors.white),
),
),
);
}
}
class ExpandedScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.purple,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Center(
child: Hero(
tag: 'radialHero',
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.purple,
child: Center(
child: Text(
'Expanded View',
style: TextStyle(
color: Colors.white,
fontSize: 32,
),
),
),
),
),
),
);
}
}
💡 Hero Animation Tips:
- Unique Tags: Each Hero must have a unique tag within the route
- Same Tag: Both screens must use the exact same tag string
- Widget Type: Works best with similar widget types (Image to Image)
- Performance: Hero animations are optimized by Flutter
- Material: Wrap with Material widget if you see rendering issues
- Placeholders: Use placeholderBuilder for loading states