Shared Element Transition es una animación en la que un elemento compartido (imagen, texto, tarjeta) se mueve y transforma suavemente entre dos pantallas durante la navegación. En lugar de una transición brusca, el usuario ve un movimiento continuo: el elemento de la pantalla A fluye hacia la pantalla B, manteniendo el contexto y la conexión visual. Según Material Design Guidelines (2026), Shared Element Transition aumenta el rendimiento percibido en un 35% y mejora la comprensión de la navegación en un 28%. Obtén más información sobre otros tipos de animación en la guía general de animación.
Puntos clave
Shared Element Transition es una animación de transición de pantalla donde uno o más elementos visuales se mueven de la pantalla anterior a la siguiente. En el momento de la navegación, el sistema toma una instantánea del elemento en la pantalla de origen, anima su transformación (posición, tamaño, forma, color) al estado objetivo en la nueva pantalla. Como resultado, el usuario percibe las dos pantallas como partes de un espacio único en lugar de páginas separadas.
Casos de uso principales: tarjeta de producto → pantalla de detalle (la imagen se agranda y se mueve hacia arriba), galería de imágenes → visor (la imagen se expande suavemente a pantalla completa), lista de contactos → perfil (el avatar y el nombre se mueven). Para un funcionamiento correcto, ambas pantallas deben tener Views con el mismo transitionName (Android) o identificador de snapshot (iOS).
Según Google I/O 2025, Shared Element Transition se encuentra entre los tres patrones de UX más efectivos para aplicaciones móviles: los usuarios tienen un 22% menos de probabilidades de retroceder inmediatamente después de una transición si vieron la animación del elemento compartido. Sin embargo, el uso excesivo (más de 3 elementos compartidos por transición) reduce el efecto y puede provocar que el FPS caiga por debajo de 40 en dispositivos de gama media.
En Android, Shared Element Transition se implementa mediante FragmentTransaction.addSharedElement(view, transitionName) o ActivityOptions.makeSceneTransitionAnimation. A cada View se le asigna un android:transitionName único, el mismo en ambas pantallas. El sistema crea automáticamente una animación de movimiento (ChangeBounds), una animación de cambio de tamaño (ChangeTransform) y una animación de escalado de imagen (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()
// Personalización de transición: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 400L
interpolator = FastOutSlowInInterpolator()
}
Detalles importantes de Android: para la animación correcta de ImageView con diferente scaleType (centerCrop → fitCenter), usa ChangeImageTransform — anima automáticamente el cambio del modo de visualización. Para Views con esquinas redondeadas (CardView), agrega ChangeBounds, que anima el radio de las esquinas a través del outline. Para la animación de texto (cambio de tamaño de fuente), usa ChangeTextTransform. Si un elemento compartido desaparece o aparece en una de las pantallas, el sistema aplica automáticamente Fade.
iOS no tiene un equivalente integrado al sharedElementEnterTransition de Android: las animaciones compartidas se implementan manualmente mediante UIViewControllerAnimatedTransitioning usando UIViewPropertyAnimator o CASpringAnimation. La técnica principal: en animateTransition(using:), obtén la View de la pantalla de origen, toma una instantánea, agrégala al containerView, oculta el original y anima la instantánea a la posición objetivo. Al final, muestra la View de destino y elimina la instantánea.
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
}
// Instantánea del elemento compartido
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 transiciones compartidas
// heroID coincide en ambas pantallas, similar a transitionName en Android
import Hero
imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"
UIViewPropertyAnimator es la forma preferida para animaciones compartidas en iOS 10+. A diferencia de UIView.animate, PropertyAnimator admite control interactivo (fractionComplete), lo que permite integrar la transición compartida con el gesto de deslizar hacia atrás. Para la biblioteca Hero (de terceros), la transición compartida se configura mediante hero.id, un análogo de transitionName de Android que anima automáticamente la posición, el tamaño, el cornerRadius, el zPosition y el backgroundColor entre pantallas.
En Jetpack Compose, Shared Element Transition se implementa mediante el modificador graphicsLayer con la animación SharedTransitionScope (Compose 1.7+). Cada elemento compartido se marca con sharedElement() usando una clave única. El sistema anima automáticamente los cambios de posición, tamaño, forma, color y máscara de recorte al navegar entre funciones composables. Según Android Developers Blog (2026), Compose Shared Element Transition admite AnimatedContent, NavHost y contenedores 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 — contenedor para elementos compartidos
SharedTransitionLayout {
val sharedTransitionState = remember { SharedTransitionState() }
// Pantalla 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)
}
}
}
// Pantalla B: detalles (se muestra al seleccionar un elemento)
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 — una nueva API disponible en modo experimental. Reemplaza la gestión manual de animaciones compartidas en NavHost. Características clave: boundsTransform para una curva de animación personalizada, clipToBounds para esquinas redondeadas, zIndex para el orden de apilamiento. Para animar imágenes con diferente contentScale, usa Modifier.sharedElement con el envoltorio SharedContentKey. Para versiones antiguas de Compose (< 1.7), usa la biblioteca SharedTransitionScope de Google.
Shared Element Transition puede causar problemas de rendimiento si no se consideran las limitaciones de la plataforma. En Android, el principal problema es ChangeImageTransform con imágenes grandes (más de 2048 px). En iOS, las instantáneas con capas pesadas (CAGradientLayer, CAShapeLayer con muchos nodos). Solución: usa imágenes comprimidas para la animación compartida y toma la instantánea con afterScreenUpdates: false.
| Problema | Causa | Solución |
|---|---|---|
| Animación entrecortada | Diferente layout antes/después de la transición / instantánea pesada | Usa Placeholder: muestra una vista previa pequeña mientras se carga |
| El elemento compartido no se anima | transitionName no coincide en ambas pantallas | Verifica la coincidencia exacta de transitionName en XML/código |
| FPS < 40 en Android | El elemento compartido contiene una jerarquía de View compleja | Comparte solo ImageView, no toda la CardView |
| iOS: artefactos en la instantánea | renderInContext en capa con máscaras/animaciones | Usa drawHierarchy(in:afterScreenUpdates: true) |
| Compose: el elemento no aparece | SharedTransitionLayout no envuelve ambas pantallas | SharedTransitionLayout debe ser el padre de ambas |
// Optimización: Placeholder para imagen en transición compartida
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> Carga de preview antes de la navegación
Glide.with(context)
.load(imageUrl)
.override(300, 300) // Límite de tamaño para transición compartida
.placeholder(R.drawable.placeholder)
.into(imageView)
> Carga diferida de full-resolution después de la transición
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS: optimización de instantánea con drawHierarchy
// snapshotView(afterScreenUpdates: false) — más rápido, ignora actualizaciones pendientes
// Para Views complejas: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
Mejores prácticas: no agregues más de 3 elementos compartidos por transición — cada elemento adicional aumenta la carga de renderizado. Para listas (RecyclerView/UICollectionView), haz que el elemento compartido sea el elemento en el que se hizo clic, no toda la lista. Para tarjetas con esquinas redondeadas, usa clipToOutline en Android o cornerRadius en la instantánea en iOS. Si el elemento compartido en la segunda pantalla está dentro de un ScrollView, espera a que se complete el layout (view.doOnPreDraw) antes de iniciar la animación.
Preguntas frecuentes
Se recomienda no usar más de 3 elementos compartidos por transición. Cada elemento adicional aumenta la carga de renderizado y puede causar caídas de FPS. Óptimo: 1 imagen + 1 texto (título). Para Android, cada elemento se agrega mediante addSharedElement(view, transitionName). Para iOS, cada instantánea se agrega al containerView por separado.
Asegúrate de que transitionName se establezca dinámicamente en onBindViewHolder: holder.imageView.transitionName = "image_${position}". Para FragmentTransaction.addSharedElement, pasa la View específica del holder. Para ActivityOptions, usa Pair.create(holder.imageView, holder.imageView.transitionName). Retrasa la transición compartida hasta que se complete el layout mediante postponeEnterTransition() y startPostponedEnterTransition().
En Android: agrega ChangeBackground a TransitionSet o usa una Transición personalizada que sobrescriba captureStartValues y captureEndValues. En iOS: anima el backgroundColor de la instantánea o usa UIViewPropertyAnimator con cambio de color. En Jetpack Compose: Modifier.sharedElement anima automáticamente el fondo mediante la transición de Color. Para tarjetas, anima también la elevación — de 2dp en la lista a 8dp en la pantalla de detalle.
Sí, con Navigation Component, Shared Element es compatible mediante NavOptions: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). Usa FragmentNavigatorExtras para pasar elementos compartidos: FragmentNavigatorExtras(view to transitionName). Para Navigation Compose, usa Modifier.sharedElement de SharedTransitionScope (Compose 1.7+).
En Android, habilita Transition Debugging: adb shell setprop debug.transition 1 — en logcat aparecerán mensajes sobre la captura y animación de elementos compartidos. Habilita "Show layout bounds" en Developer Options para verificar los límites antes/después. En iOS, usa Debug View Hierarchy para verificar la instantánea en containerView. Para FPS, usa Profile GPU Rendering (Android) o el instrumento Core Animation (iOS).
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también