Principais pontos
Hero Animation é uma técnica de animação de interface onde um elemento compartilhado se move suavemente de uma tela de origem para uma tela de destino, preservando sua identidade. O nome reflete a metáfora de um “héroi” — um elemento-chave que passa por uma transição roteirizada entre telas, atraindo a atenção do usuário. Ao contrário das transições padrão, Hero Animation não quebra a conexão visual: o usuário vê uma trajetória contínua do elemento, o que melhora a compreensão da hierarquia de navegação.
O conceito de Hero Animation está estabelecido no Material Design como recomendação para telas de lista-detalhe, galerias e cartões. O algoritmo de animação consiste em três fases: na tela de origem, o elemento é destacado e fixado em uma posição absoluta; durante a transição, ele escala, move e muda de forma de acordo com o contêiner de destino; na tela final, o elemento é incorporado ao novo layout. De acordo com Material Design 3, uma animação hero implementada corretamente reduz o número de pressionamentos errôneos do botão “voltar” em 22%.
No Flutter, Hero Animation é implementada através do widget Hero, que recebe um parâmetro obrigatório tag — um identificador único. O Flutter encontra automaticamente um par de widgets Hero com a mesma tag nas rotas de origem e destino e inicia a animação. O widget Hero não requer gerenciamento manual de animação — o framework calcula independentemente a trajetória e aplica a transformação através do objeto interno FlightShuttle.
FlightShuttle é um “transporte” invisível que, no momento da transição, cria uma cópia sobreposta do widget Hero, a posiciona em um sistema de coordenadas absoluto e anima a transformação (escala, translação, rotação) da posição inicial para a final. Curvas personalizadas podem ser passadas para FlightShuttle através da propriedade flightShuttleBuilder, permitindo controlar a trajetória do voo: voar em arco, quicar nas bordas ou desacelerar antes da chegada. No Flutter 3.16+, Hero suporta animação preservando clipRect e borderRadius, proporcionando o efeito de cartões arredondados se transformando em telas retangulares.
No SwiftUI, o equivalente de Hero Animation é o modificador matchedGeometryEffect, introduzido no iOS 14. Ele vincula dois elementos através de um identificador (id) e um namespace. Ao contrário do Flutter, onde Hero intercepta automaticamente a transição, SwiftUI requer declaração explícita de namespace e aplicação do modificador a ambas as partes da cena — origem e destino — dentro da mesma hierarquia ou através de GeometryReader.
A animação matchedGeometryEffect suporta transformação de posição, tamanho, rotação e raio de canto. Para suavidade, uma Animation é especificada em withAnimation. SwiftUI interpola automaticamente os estados intermediários com base na curva padrão easeInOut. No iOS 17+, foi adicionado suporte para matchedTransitionSource para TabView e NavigationStack — um análogo de sistema de Hero Animation com trajetória predefinida para transições padrão.
No Android, Hero Animation é chamada de Shared Element Transition e é implementada através de ActivityOptions.makeSceneTransitionAnimation ou FragmentTransaction.addSharedElement. Cada elemento compartilhado é marcado com um atributo transitionName em XML ou através de código, e o Android gerencia a animação entre atividades. A partir do Android 5.0 (API 21), as transições se tornaram parte do framework; antes disso, animações personalizadas tinham que ser escritas usando ValueAnimator.
No Jetpack Compose, Shared Element Transition é implementada através dos modificadores Modifier.sharedElement e Modifier.sharedBounds em conjunto com AnimatedContent ou SharedTransitionLayout (disponível desde Compose BOM 2024.01). Ao contrário do sistema View, o Compose redesenha os elementos preguiçosamente, então o elemento compartilhado é armazenado em cache no CompositionLocal. O Compose também suporta animação de lista para detalhe com preservação de scrollOffset — uma capacidade chave para transições Hero. De acordo com Google I/O 2024, Compose Shared Element Transition é 15% mais rápido que a versão View devido ao gerenciamento direto de RenderNode.
Um exemplo básico de Hero Animation no Flutter: uma miniatura na tela de lista e uma imagem em tela cheia na tela de detalhes. A tag image-hero vincula os dois widgets, e FlightShuttle anima automaticamente a transição do cartão para tela cheia.
import 'package:flutter/material.dart';
class ListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
Hero(
tag: 'image-hero',
child: GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute(
builder: (_) => DetailScreen(),
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
'https://picsum.photos/200',
width: 100, height: 100,
fit: BoxFit.cover,
),
),
),
),
],
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail')),
body: Center(
child: Hero(
tag: 'image-hero',
child: Image.network(
'https://picsum.photos/800',
width: 300, height: 400,
fit: BoxFit.cover,
),
),
),
);
}
}
O ponto chave é a mesma tag (image-hero) em ambas as telas. O Flutter corresponde os widgets Hero por tag durante a transição de rota. Se as tags diferirem, a animação não será iniciada — o elemento simplesmente aparecerá na nova tela sem transição. Qualquer tipo pode ser usado para a tag, mas strings são a opção mais confiável.
Um exemplo de Hero Animation no SwiftUI com NavigationStack. O modificador matchedGeometryEffect vincula a imagem na lista e a tela de detalhes através de um namespace, e withAnimation desencadeia uma transição suave.
import SwiftUI
struct HeroExampleView: View {
@Namespace private var heroNamespace
@State private var isDetailShown = false
var body: some View {
VStack {
if isDetailShown {
DetailView(namespace: heroNamespace)
.transition(.identity)
} else {
ListView(namespace: heroNamespace)
.transition(.identity)
}
}
.animation(Animation.easeInOut(duration: 0.4),
value: isDetailShown)
}
}
struct ListView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 100, height: 100)
.cornerRadius(12)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
struct DetailView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 300, height: 400)
.cornerRadius(0)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
@Namespace cria um espaço de nomes para o identificador matchedGeometryEffect. Ambos os elementos usam o mesmo id (“hero”) e um namespace compartilhado. Quando isDetailShown alterna, SwiftUI anima a transformação de tamanho, posição e raio de canto entre as duas visualizações, criando o efeito hero. Para um NavigationLink padrão com iOS 16+, use navigationTransition(.hero).
Um exemplo de Shared Element Transition no Jetpack Compose com SharedTransitionLayout. O modificador sharedElement anima um elemento compartilhado entre telas durante a navegação.
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun HeroExample() {
SharedTransitionLayout {
var isDetail by remember { mutableStateOf(false) }
AnimatedContent(
targetState = isDetail,
transitionSpec = {
fadeIn() togetherWith fadeOut()
}
) { detail ->
if (detail) {
Box(
modifier = Modifier
.fillMaxSize()
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/800",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
} else {
Box(
modifier = Modifier
.size(100.dp)
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/200",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
}
Compose SharedTransitionLayout (Compose BOM 2024.01+) envolve a hierarquia, e o modificador sharedElement com a chave “hero” vincula os elementos inicial e final. AnimatedContent alterna o estado, e o Compose anima automaticamente o tamanho, posição e forma da imagem. Uma vantagem chave é o suporte para animação de bounds e clipBounds, proporcionando o efeito de arredondamento de cartão durante a transição.
Perguntas frequentes
Sim, Hero Animation está disponível em ambas as plataformas. O iOS usa matchedGeometryEffect no SwiftUI ou UINavigationController.transitioningDelegate. O Android usa ActivityOptions.makeSceneTransitionAnimation ou Shared Element Transition no Jetpack Compose com Modifier.sharedElement. O Flutter implementa Hero Animation através do widget Hero, funcionando de forma idêntica em ambas as plataformas.
Sim, Hero Animation suporta múltiplos elementos compartilhados. No Flutter, especifique vários widgets Hero com tags diferentes. No SwiftUI, use matchedGeometryEffect com identificadores diferentes em cada elemento. Todas as animações são executadas em paralelo — cada elemento viaja em sua própria trajetória.
O desempenho depende da implementação. FlightShuttle do Flutter processa imagens através de RenderObject, alcançando 60fps. matchedGeometryEffect do SwiftUI é vetorizado. Para imagens grandes, use precacheImage no Flutter ou prefetch no SwiftUI — isso elimina atrasos na primeira transição. Android Compose requer carregamento diferido através de AsyncImage.
Uma transição push padrão desliza a tela inteira, criando a ilusão de uma pilha de cartões. Hero Animation fixa a atenção visual em um único elemento — o usuário vê seu movimento contínuo, melhorando a compreensão da relação entre as telas. Push é bom para navegação hierárquica, Hero para enfatizar o conteúdo.
Não, Hero Animation é implementada com ferramentas integradas. No Flutter, é o widget Hero do pacote flutter/material.dart. No SwiftUI, é o modificador matchedGeometryEffect. No Android Jetpack Compose, é SharedTransitionLayout e Modifier.sharedElement do Compose padrão. Bibliotecas adicionais são necessárias apenas para trajetórias não padrão ou transformações complexas.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também