Animating Widgets in Flutter Grids

Part I: Page transitions using the hero widget

Danielle H
4 min readSep 15, 2023

Sometimes, you need to transition between two different kinds of grids in your app. For example, you have a grid of images and the user wants to see only the user’s favorites. Some widgets are the same, some aren’t. How can you animate this so that the existing widgets move and resize within the grid?

That’s the subject of this series. We will explore three ways to do this (knowing Flutter, there are probably 100 more 😆)

  1. Using Hero between pages (this article)
  2. Using AnimatedPositioned in a Stack (next week’s article)
  3. Using Flow widget (here)

Our screens:

No hero :(

It works fine, but it’s lacking… that extra something. Ideally, we want each image to move to its corresponding new location, resizing as it goes.

And Flutter has a simple, easy built-in solution for this: it’s called the Hero widget.

If you have the same widget appearing in two pages, the Hero widget will automatically animate its transition between them. You have to do almost nothing.

Basically, you just need to give a name to each widget. If you use the same name in two screens — Hero takes care of the rest. A hero indeed!

And then we have the smooth transition we were looking for:

Smoooooth

So let’s see the code.

Original code

The first screen:

import 'package:flutter/material.dart';
import 'package:gridview_transitions/hero/hero_second.dart';

import '../widgets/photo_widget.dart';

//First screen
class HeroFirst extends StatelessWidget {
HeroFirst({super.key});

//tapping any image moves to the next screen
void onTap(String name, BuildContext context) {
print(name);
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return HeroSecond();
}));
}

@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(title: const Text("Hero the first")),
//create our grid using GridView
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: 4,
itemBuilder: (BuildContext context, int index) {
String name = "tree${index + 2}";//name of the photo
return PhotoWidget(
onTap: () {
onTap(name, context);
},
name: name,
);
},
),
),
);
}
}

Second screen:

import 'package:flutter/material.dart';

import '../widgets/photo_widget.dart';

//Second screen
class HeroSecond extends StatelessWidget {
const HeroSecond({super.key});

//tapping any image moves back to the first screen
void onTap(String name, BuildContext context) {
print(name);
Navigator.of(context).pop();
}

@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(title: const Text("Hero the second")),
//create our grid using Gridview again, different axis count
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemCount: 8,
itemBuilder: (BuildContext context, int index) {
String name = "tree${index + 1}";//name of the photo
return PhotoWidget(
onTap: () {
onTap(name, context);
},
name: name,
);
},
),
),
);
}
}

PhotoWidget:

import 'package:flutter/material.dart';

class PhotoWidget extends StatelessWidget {
const PhotoWidget({super.key, required this.onTap, required this.name});

final VoidCallback onTap; //what to do when tapped
final String name; //name of the photo in assets

@override
Widget build(BuildContext context) {
return Material(
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Image.asset(
'images/$name.jpg',
fit: BoxFit.contain,
),
),
));
}
}

my images folder looks like this:

Can’t see the forest for the trees.

So the PhotoWidget gets the image from images folder and displays it nicely. The first screen shows 4 images in a 2x2 grid; the second screen shows 8 images in a 3x3 grid. Functionality works great, but lacks pizzazz.

Hero code

The only modification we need to do is wrap our PhotoWidget in a Hero widget and give it a name. Coincidentally, we already have a name — the name of the photo. So we can use that. If you prefer, you can create names based on other parameters, as long as they are unique to each widget.

PhotoWidget:

import 'package:flutter/material.dart';

class PhotoWidget extends StatelessWidget {
const PhotoWidget({super.key, required this.onTap, required this.name});

final VoidCallback onTap; //what to do when tapped
final String name; //name of the photo in assets

@override
Widget build(BuildContext context) {
return Hero( //<-- See this?
tag: name, //<-- and this?
child: Material(
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Image.asset(
'images/$name.jpg',
fit: BoxFit.contain,
),
),
)),
);
}
}

And that’s it! I did say it was easy :)

There is a (small) catch with the Hero widget. It activates only when pages are pushed and popped from the Navigator. So if your two grids are two widgets in the same page, or transitioned using a TabBarView… Hero doesn’t work.

Bummer.

But fear not, because the next two articles have solutions for exactly this problem! Though I admit, they are just a tad more complex than this…

Full code here:

See you next time :)

Image by bess.hamiti@gmail.com from Pixabay. Changed hue for different images using Gimp. Screen gifs edited using Screen2Gif.

--

--

Danielle H

I started programming in LabVIEW and Matlab, and quickly expanded to include Android, Swift, Flutter, Web(PHP, HTML, Javascript), Arduino and Processing.