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

🧠 Test Your Knowledge

What must be the same on both screens for Hero animation to work?