Shared Element Transition é uma animação onde um elemento compartilhado (imagem, texto, cartão) se move e se transforma suavemente entre duas telas durante a navegação. Em vez de uma transição abrupta, o usuário vê um movimento contínuo: o elemento da tela A flui para a tela B, preservando o contexto e a conexão visual. De acordo com as Material Design Guidelines (2026), o Shared Element Transition aumenta o desempenho percebido em 35% e melhora a compreensão da navegação em 28%. Saiba mais sobre outros tipos de animação no guia geral de animação.
Pontos principais
Shared Element Transition é uma animação de transição de tela onde um ou mais elementos visuais se movem da tela anterior para a seguinte. No momento da navegação, o sistema tira um snapshot do elemento na tela de origem, anima sua transformação (posição, tamanho, forma, cor) para o estado de destino na nova tela. Como resultado, o usuário percebe as duas telas como partes de um único espaço, em vez de páginas separadas.
Principais casos de uso: cartão de produto → tela de detalhes (a imagem amplia e sobe), galeria de imagens → visualizador (a imagem se expande suavemente para tela cheia), lista de contatos → perfil (avatar e nome se movem). Para funcionar corretamente, ambas as telas devem ter Views com o mesmo transitionName (Android) ou identificador de snapshot (iOS).
De acordo com o Google I/O 2025, o Shared Element Transition está entre os três padrões de UX mais eficazes para aplicativos móveis: os usuários têm 22% menos probabilidade de voltar imediatamente após uma transição se viram a animação do elemento compartilhado. No entanto, o uso excessivo (mais de 3 elementos compartilhados por transição) reduz o efeito e pode fazer o FPS cair abaixo de 40 em dispositivos de gama média.
No Android, o Shared Element Transition é implementado via FragmentTransaction.addSharedElement(view, transitionName) ou ActivityOptions.makeSceneTransitionAnimation. Cada View recebe um android:transitionName único — o mesmo em ambas as telas. O sistema cria automaticamente uma animação de movimento (ChangeBounds), uma animação de mudança de tamanho (ChangeTransform) e uma animação de escalonamento de imagem (ChangeImageTransform).
<androidx.cardview.widget.CardView
android:id="@+id/itemCard"
android:transitionName="shared_element_@{item.id}"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/itemImage"
android:transitionName="shared_image_@{item.id}" />
</androidx.cardview.widget.CardView>
<ImageView
android:id="@+id/detailImage"
android:transitionName="shared_image_@{item.id}"
android:layout_width="match_parent"
android:layout_height="300dp" />
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair
import androidx.fragment.app.FragmentTransaction
// Activity: Shared Element via ActivityOptions
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
Pair(imageView, getString(R.string.transition_name_image)),
Pair(titleView, getString(R.string.transition_name_title))
)
startActivity(intent, options.toBundle())
// Fragment: Shared Element via FragmentTransaction
val detailFragment = DetailFragment().apply {
sharedElementEnterTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.move)
sharedElementReturnTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.move)
}
supportFragmentManager.beginTransaction()
.addSharedElement(imageView, imageView.transitionName)
.addSharedElement(titleView, titleView.transitionName)
.replace(R.id.container, detailFragment)
.addToBackStack(null)
.commit()
// Personalização de transição: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 400L
interpolator = FastOutSlowInInterpolator()
}
Detalhes importantes do Android: para a animação correta de ImageView com scaleType diferente (centerCrop → fitCenter), use ChangeImageTransform — ele anima automaticamente a mudança do modo de exibição. Para Views com cantos arredondados (CardView), adicione ChangeBounds, que anima o raio dos cantos via outline. Para animação de texto (mudança de tamanho da fonte), use ChangeTextTransform. Se um elemento compartilhado desaparece ou aparece em uma das telas, o sistema aplica automaticamente o Fade.
O iOS não tem um equivalente nativo ao sharedElementEnterTransition do Android — as animações compartilhadas são implementadas manualmente via UIViewControllerAnimatedTransitioning usando UIViewPropertyAnimator ou CASpringAnimation. A técnica principal: em animateTransition(using:), obtenha a View da tela de origem, tire um snapshot, adicione-o ao containerView, oculte o original e anime o snapshot para a posição de destino. No final, mostre a View de destino e remova o snapshot.
import UIKit
"> Animador de Shared Element personalizado
class SharedElementAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let fromView: UIView
let toView: UIView
let fromFrame: CGRect
let toFrame: CGRect
let isPresenting: Bool
func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using ctx: UIViewControllerContextTransitioning) {
guard let toVC = ctx.viewController(forKey: .to),
let fromVC = ctx.viewController(forKey: .from)
else { return }
let container = ctx.containerView
if isPresenting {
container.addSubview(toVC.view)
toVC.view.layoutIfNeeded()
toVC.view.alpha = 0
}
// Snapshot do elemento compartilhado
let snapshot = fromView.snapshotView(afterScreenUpdates: false)!
snapshot.frame = isPresenting ? fromFrame : toFrame
snapshot.layer.cornerRadius = isPresenting ? 12 : 0
container.addSubview(snapshot)
fromView.isHidden = true
toView.isHidden = true
UIView.animate(withDuration: transitionDuration(using: ctx),
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0) {
snapshot.frame = self.isPresenting ? self.toFrame : self.fromFrame
snapshot.layer.cornerRadius = self.isPresenting ? 0 : 12
toVC.view.alpha = 1
} completion: { _ in
fromView.isHidden = false
toView.isHidden = false
snapshot.removeFromSuperview()
ctx.completeTransition(!ctx.transitionWasCancelled)
}
}
}
// Hero — biblioteca para simplificar transições compartilhadas
// heroID corresponde em ambas as telas, similar ao transitionName no Android
import Hero
imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"
UIViewPropertyAnimator é a maneira preferida para animações compartilhadas no iOS 10+. Ao contrário do UIView.animate, o PropertyAnimator suporta controle interativo (fractionComplete), permitindo integrar a transição compartilhada com o gesto de deslizar para trás. Para a biblioteca Hero (terceiros), a transição compartilhada é configurada via hero.id — um análogo do transitionName do Android, que anima automaticamente posição, tamanho, cornerRadius, zPosition e backgroundColor entre telas.
No Jetpack Compose, o Shared Element Transition é implementado através do modificador graphicsLayer com a animação SharedTransitionScope (Compose 1.7+). Cada elemento compartilhado é marcado com sharedElement() usando uma chave única. O sistema anima automaticamente as mudanças de posição, tamanho, forma, cor e máscara de recorte ao navegar entre funções composable. De acordo com o Android Developers Blog (2026), o Compose Shared Element Transition suporta AnimatedContent, NavHost e contêineres personalizados.
import androidx.compose.animation.core.spring
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
// SharedTransitionLayout — contêiner para elementos compartilhados
SharedTransitionLayout {
val sharedTransitionState = remember { SharedTransitionState() }
// Tela A: lista
LazyColumn {
items(items) { item ->
Card(
modifier = Modifier.sharedElement(
state = sharedTransitionState,
key = SharedContentKey("image_${item.id}"),
boundsTransform = { _, _ ->
spring(dampingRatio = Spring.DampingRatioMediumBouncy)
}
)
) {
AsyncImage(model = item.imageUrl, contentDescription = null)
}
}
}
// Tela B: detalhes (mostrada quando um elemento é selecionado)
AnimatedContent(
targetState = selectedItem,
transitionSpec = {
EnterTransition.None using SizeTransform(clip = false)
}
) { item ->
item?.let {
Image(
modifier = Modifier.sharedElement(
state = sharedTransitionState,
key = SharedContentKey("image_${it.id}")
),
painter = rememberAsyncImagePainter(it.imageUrl),
contentDescription = null,
contentScale = ContentScale.FillWidth
)
}
}
}
Compose 1.7+ SharedTransitionScope — uma nova API disponível em modo experimental. Ela substitui o gerenciamento manual de animações compartilhadas no NavHost. Principais recursos: boundsTransform para curva de animação personalizada, clipToBounds para cantos arredondados, zIndex para ordem de empilhamento. Para animar imagens com contentScale diferente, use Modifier.sharedElement com o wrapper SharedContentKey. Para versões antigas do Compose (< 1.7), use a biblioteca SharedTransitionScope do Google.
Shared Element Transition pode causar problemas de desempenho se as limitações da plataforma não forem consideradas. No Android, o principal problema é o ChangeImageTransform com imagens grandes (acima de 2048 px). No iOS — snapshot com camadas pesadas (CAGradientLayer, CAShapeLayer com muitos nós). Solução: use imagens compactadas para animação compartilhada e tire o snapshot com afterScreenUpdates: false.
| Problema | Causa | Solução |
|---|---|---|
| Animação com travamentos | Layout diferente antes/depois da transição / snapshot pesado | Use Placeholder: mostre uma prévia pequena enquanto carrega |
| Elemento compartilhado não anima | transitionName não corresponde em ambas as telas | Verifique a correspondência exata do transitionName no XML/código |
| FPS < 40 no Android | Elemento compartilhado contém hierarquia de View complexa | Compartilhe apenas ImageView, não a CardView inteira |
| iOS: artefatos no snapshot | renderInContext em camada com máscaras/animações | Use drawHierarchy(in:afterScreenUpdates: true) |
| Compose: elemento não aparece | SharedTransitionLayout não envolve ambas as telas | SharedTransitionLayout deve ser o pai de ambas |
// Otimização: Placeholder para imagem em transição compartilhada
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> Carregamento do preview antes da navegação
Glide.with(context)
.load(imageUrl)
.override(300, 300) // Limite de tamanho para transição compartilhada
.placeholder(R.drawable.placeholder)
.into(imageView)
> Carregamento tardio de full-resolution após a transição
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS: otimização de snapshot com drawHierarchy
// snapshotView(afterScreenUpdates: false) — mais rápido, ignora atualizações pendentes
// Para Views complexas: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
Melhores práticas: não adicione mais de 3 elementos compartilhados por transição — cada elemento adicional aumenta a carga de renderização. Para listas (RecyclerView/UICollectionView), torne o elemento compartilhado no item clicado, não na lista inteira. Para cartões com cantos arredondados, use clipToOutline no Android ou cornerRadius no snapshot no iOS. Se o elemento compartilhado na segunda tela estiver dentro de um ScrollView, aguarde a conclusão do layout (view.doOnPreDraw) antes de iniciar a animação.
Perguntas frequentes
Recomenda-se não usar mais de 3 elementos compartilhados por transição. Cada elemento adicional aumenta a carga de renderização e pode causar quedas de FPS. Ideal: 1 imagem + 1 texto (título). No Android, cada elemento é adicionado via addSharedElement(view, transitionName). No iOS, cada snapshot é adicionado ao containerView separadamente.
Certifique-se de que transitionName seja definido dinamicamente no onBindViewHolder: holder.imageView.transitionName = "image_${position}". Para FragmentTransaction.addSharedElement, passe a View específica do holder. Para ActivityOptions, use Pair.create(holder.imageView, holder.imageView.transitionName). Atrase a transição compartilhada até a conclusão do layout via postponeEnterTransition() e startPostponedEnterTransition().
No Android: adicione ChangeBackground ao TransitionSet ou use uma Transition personalizada substituindo captureStartValues e captureEndValues. No iOS: anime o backgroundColor do snapshot ou use UIViewPropertyAnimator com mudança de cor. No Jetpack Compose: Modifier.sharedElement anima automaticamente o fundo via transição de Color. Para cartões, anime também a elevação — de 2dp na lista para 8dp na tela de detalhes.
Sim, com o Navigation Component, o Shared Element é suportado via NavOptions: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). Use FragmentNavigatorExtras para passar elementos compartilhados: FragmentNavigatorExtras(view to transitionName). Para Navigation Compose, use Modifier.sharedElement do SharedTransitionScope (Compose 1.7+).
No Android, ative o Transition Debugging: adb shell setprop debug.transition 1 — no logcat aparecerão mensagens sobre a captura e animação de elementos compartilhados. Ative "Show layout bounds" nas Developer Options para verificar os limites antes/depois. No iOS, use o Debug View Hierarchy para verificar o snapshot no containerView. Para FPS, use o Profile GPU Rendering (Android) ou o instrumento Core Animation (iOS).
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