Shared Element Transition — qué es, elementos compartidos en transiciones

Autor: IT Sectr Publicado: 2026-03-02 Tiempo de lectura: 10 min

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 — animación de un elemento compartido entre dos pantallas durante la navegación.
  • sharedElementEnterTransition — atributo XML de Android para configurar la entrada del elemento compartido.
  • UIViewPropertyAnimator — base para animaciones compartidas personalizadas entre UIViewControllers en iOS.
  • El elemento compartido se define mediante transitionName en Android y tag o snapshot en iOS.
  • La animación funciona con ImageView, TextView, CardView, Button y cualquier View con un identificador único.

¿Qué es Shared Element Transition?

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.

Shared Element Transition en Android (Kotlin)

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).

xml

<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" />
kotlin
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.

Shared Element Transition en iOS (Swift)

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.

swift
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.

Shared Element en Jetpack Compose

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.

kotlin
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.

Optimización y problemas comunes

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.

ProblemaCausaSolución
Animación entrecortadaDiferente layout antes/después de la transición / instantánea pesadaUsa Placeholder: muestra una vista previa pequeña mientras se carga
El elemento compartido no se animatransitionName no coincide en ambas pantallasVerifica la coincidencia exacta de transitionName en XML/código
FPS < 40 en AndroidEl elemento compartido contiene una jerarquía de View complejaComparte solo ImageView, no toda la CardView
iOS: artefactos en la instantánearenderInContext en capa con máscaras/animacionesUsa drawHierarchy(in:afterScreenUpdates: true)
Compose: el elemento no apareceSharedTransitionLayout no envuelve ambas pantallasSharedTransitionLayout debe ser el padre de ambas
kotlin
// 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

¿Cuántos elementos compartidos se pueden animar en una transición?

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.

Shared Element Transition no funciona con RecyclerView — ¿qué hacer?

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().

¿Cómo hacer Shared Element Transition con cambio de color de fondo?

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.

¿Funciona Shared Element en Android con Jetpack Navigation?

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+).

¿Cómo depurar Shared Element Transition?

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

  • Shared Element Transition — animación de un elemento compartido entre pantallas preservando el contexto visual.
  • Android: FragmentTransaction.addSharedElement o ActivityOptions.makeSceneTransitionAnimation.
  • iOS: implementación personalizada mediante UIViewControllerAnimatedTransitioning + snapshot + PropertyAnimator.
  • Jetpack Compose: Modifier.sharedElement de SharedTransitionScope (Compose 1.7+).
  • Máximo 3 elementos compartidos por transición para evitar caídas de FPS.
  • Para ImageView con diferente scaleType, usa ChangeImageTransform (Android) o snapshot (iOS).
  • Biblioteca Hero (iOS) — solución de terceros con hero.id, similar a transitionName en Android.

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.

Discutir el proyecto

Lea también