Make a hero animation in Flutter
How to add a hero animation to your Flutter app? Why doesn't it work?
It is sometimes very useful to animate a piece of UI when transitioning between widgets.
Flutter's Hero widget can be used to provide an animation like this:
To add it to your app, simply wrap your existing widget in Hero
:
import 'package:flutter/material.dart';
class MenuImage extends StatelessWidget {
final String photoUrl;
final int itemId;
const MenuImage({
required this.itemId,
required this.photoUrl,
super.key,
});
@override
Widget build(BuildContext context) {
return Hero(
tag: "menuItem$itemId",
child: Image.network(
photoUrl,
fit: BoxFit.cover,
semanticLabel: "$itemId's photo",
),
);
}
}
And make sure tag
property is the same for both widgets!
(this code if from Make a food delivery app with DollarBackend (2/3))
Hero widget doesn't work
Here's an example when Hero widget doesn't work in Flutter (you can copy and run it on dartpad):
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
home: FirstWidget(),
);
}
}
class FirstWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Hero(
tag: "image",
child: Image.network("https://httpbin.org/image/jpeg"),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SecondWidget()));
},
child: const Text("next"))
],
),
),
);
}
}
class SecondWidget extends StatefulWidget {
@override
State<SecondWidget> createState() => _SecondWidgetState();
}
class _SecondWidgetState extends State<SecondWidget> {
String? imgUrl;
@override
void initState() {
super.initState();
Future.delayed(const Duration(milliseconds: 10), () {
setState(() {
imgUrl = "https://httpbin.org/image/jpeg";
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Stack(
alignment: Alignment.topCenter,
children: [
if (imgUrl != null)
Hero(
tag: "image",
child: Image.network(imgUrl!),
),
const Align(
alignment: Alignment.bottomCenter,
child: Text("hero works!"),
),
],
),
),
);
}
}
As you can see, we're trying to switch from one page to another, but Hero widget doesn't work.
The reason being, it takes some time for imgUrl
to become available on SecondWidget
.
The widget gets rendered initially, there is no hero.
Animation starts, then imgUrl
becomes available, so the widget gets re-rendered.
It looks like the animation doesn't work, but in reality the animation doesn't see another Hero
on the first page.
How to fix
Make sure that you have a Hero widget on the first build
.
For this example, passing imgUrl
from one widget to another as a parameter resolves the problem.
Working code:
Having to debug both your app and your server is no fun!
We can host an open source backend for you! No need to set up and maintain your own server. Sign up here, no credit card required
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
home: FirstWidget(),
);
}
}
const imgOne = "https://httpbin.org/image/jpeg";
class FirstWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Hero(
tag: "image",
child: Image.network(imgOne),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondWidget(imgUrl: imgOne)),
);
},
child: const Text("next"))
],
),
),
);
}
}
class SecondWidget extends StatefulWidget {
String? imgUrl;
SecondWidget({required this.imgUrl, super.key});
@override
State<SecondWidget> createState() => _SecondWidgetState();
}
class _SecondWidgetState extends State<SecondWidget> {
String? _imgUrl;
@override
void initState() {
super.initState();
// _imgUrl should be set on the first run;
_imgUrl = widget.imgUrl;
Future.delayed(const Duration(milliseconds: 10), () {
// and then it can be obtained from another source
// (and probably in a higher resolution)
setState(() {
_imgUrl = "https://httpbin.org/image/png";
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Stack(
alignment: Alignment.topCenter,
children: [
if (_imgUrl != null)
Hero(
tag: "image",
child: Image.network(_imgUrl!),
),
const Align(
alignment: Alignment.bottomCenter,
child: Text("hero works!"),
),
],
),
),
);
}
}