Shared Element Transition — ce este, elemente comune în tranziții

Autor: IT Sectr Publicat: 2026-03-02 Timp de citire: 10 min

Shared Element Transition — animația prin care un element comun (imagine, text, card) se deplasează și se transformă lin între două ecrane în timpul navigării. În locul unei tranziții bruște, utilizatorul vede o mișcare continuă: elementul de pe ecranul A «curge» pe ecranul B, păstrând contextul și legătura vizuală. Conform Material Design Guidelines (2026), Shared Element Transition crește performanța percepută cu 35% și îmbunătățește înțelegerea navigării cu 28%. Citiți mai multe despre alte tipuri de animații în ghidul general de animație.

Principalele puncte

  • Shared Element Transition — animația unui element comun între două ecrane în timpul navigării.
  • sharedElementEnterTransition — atributul XML Android pentru configurarea intrării elementului comun.
  • UIViewPropertyAnimator — baza animațiilor partajate personalizate între UIViewController în iOS.
  • Elementul comun se setează prin transitionName în Android și tag sau snapshot în iOS.
  • Animația funcționează cu ImageView, TextView, CardView, Button și orice View cu un identificator unic.

Ce este Shared Element Transition?

Shared Element Transition — este o animație de schimbare a ecranelor în care unul sau mai multe elemente vizuale «se mută» de pe ecranul anterior pe cel următor. În momentul navigării, sistemul face un snapshot al elementului pe ecranul sursă, animă transformarea acestuia (poziție, dimensiune, formă, culoare) către starea țintă pe noul ecran. Ca rezultat, utilizatorul percepe cele două ecrane ca părți ale unui spațiu unitar, nu ca pagini separate.

Principalele scenarii de utilizare: card de produs → ecran detalii (imaginea se mărește și trece sus), galerie de imagini → vizualizare (imaginea se deschide lin pe tot ecranul), listă de contacte → profil (avatarul și numele se deplasează). Pentru funcționarea corectă, ambele ecrane trebuie să aibă un View cu același transitionName (Android) sau identificator snapshot (iOS).

Conform Google I/O 2025, Shared Element Transition se află printre cele mai eficiente trei modele UX pentru aplicații mobile: utilizatorii se întorc cu 22% mai rar imediat după tranziție dacă au văzut animația elementului comun. Cu toate acestea, utilizarea excesivă (mai mult de 3 elemente partajate per tranziție) reduce efectul și poate cauza scăderea FPS sub 40 pe dispozitivele de gamă medie.

Shared Element Transition în Android (Kotlin)

Pe Android, Shared Element Transition se implementează prin FragmentTransaction.addSharedElement(view, transitionName) sau ActivityOptions.makeSceneTransitionAnimation. Fiecărui View i se atribuie un android:transitionName unic — același pe ambele ecrane. Sistemul creează automat o animație de deplasare (ChangeBounds), schimbare de dimensiune (ChangeTransform) și scalare a imaginii (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 prin 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 prin 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()

// Personalizarea tranziției: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
    ordering = TransitionSet.ORDERING_TOGETHER
    addTransition(ChangeBounds())
    addTransition(ChangeTransform())
    addTransition(ChangeImageTransform())
    duration = 400L
    interpolator = FastOutSlowInInterpolator()
}

Detalii importante Android: pentru animarea corectă a ImageView cu scaleType diferit (centerCrop → fitCenter) folosiți ChangeImageTransform — acesta animează automat schimbarea modului de afișare. Pentru View cu colțuri rotunjite (CardView) adăugați ChangeBounds, care animează raza de rotunjire prin outline. Pentru animarea textului (schimbarea dimensiunii fontului) folosiți ChangeTextTransform. Dacă elementul partajat dispare sau apare pe unul dintre ecrane — sistemul aplică automat Fade.

Shared Element Transition în iOS (Swift)

Pe iOS nu există un echivalent încorporat al sharedElementEnterTransition din Android — animațiile partajate se implementează manual prin UIViewControllerAnimatedTransitioning folosind UIViewPropertyAnimator sau CASpringAnimation. Tehnica principală: în animateTransition(using:) luați View-ul ecranului sursă, faceți un snapshot, adăugați-l în containerView, ascundeți originalul și animați snapshot-ul către poziția țintă. La final, afișați View-ul țintă și eliminați snapshot-ul.

swift
import UIKit

"> Animator personalizat Shared Element
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 al elementului comun
        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 — bibliotecă pentru simplificarea shared transitions
// heroID se potrivește pe ambele ecrane, analog transitionName în Android
import Hero

imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"

UIViewPropertyAnimator — metoda preferată pentru animațiile partajate pe iOS 10+. Spre deosebire de UIView.animate, PropertyAnimator suportă controlul interactiv (fractionComplete), permițând integrarea shared transition cu gestul swipe-back. Pentru biblioteca Hero (terță parte), shared transition se setează prin hero.id — acesta este analogul transitionName din Android, care animează automat poziția, dimensiunea, cornerRadius, zPosition și backgroundColor între ecrane.

Shared Element în Jetpack Compose

În Jetpack Compose, Shared Element Transition se implementează prin modificatorul graphicsLayer cu animația SharedTransitionScope (Compose 1.7+). Fiecare element partajat este marcat prin sharedElement() cu o cheie unică. Sistemul animează automat schimbarea poziției, dimensiunii, formei, culorii și măștii de decupare în timpul navigării între funcțiile composable. Conform Android Developers Blog (2026), Compose Shared Element Transition suportă AnimatedContent, NavHost și containere personalizate.

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 — container pentru elemente partajate
SharedTransitionLayout {
    val sharedTransitionState = remember { SharedTransitionState() }

    // Ecranul A: listă
    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)
            }
        }
    }

    // Ecranul B: detalii (se afișează la selectarea elementului)
    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 — API nou disponibil în modul experimental. Înlocuiește gestionarea manuală a animațiilor partajate în NavHost. Capabilități cheie: boundsTransform pentru curba de animație personalizată, clipToBounds pentru colțuri rotunjite, zIndex pentru ordinea de suprapunere. Pentru animarea imaginilor cu contentScale diferit, folosiți Modifier.sharedElement cu wrapper-ul SharedContentKey. Pentru suportul versiunilor mai vechi de Compose (< 1.7) folosiți biblioteca SharedTransitionScope de la Google.

Optimizare și probleme tipice

Shared Element Transition poate cauza probleme de performanță dacă nu se ține cont de limitările platformei. Pe Android, problema principală este ChangeImageTransform cu imagini mari (peste 2048 px). Pe iOS — snapshot cu straturi grele (CAGradientLayer, CAShapeLayer cu multe noduri). Soluție: folosiți imagini comprimate pentru animațiile partajate și faceți snapshot cu afterScreenUpdates: false.

ProblemăCauzăSoluție
Animație sacadatăLayout diferit înainte și după tranziție / snapshot greuFolosiți Placeholder: afișați o previzualizare mică la încărcare
Elementul partajat nu se animeazătransitionName nu se potrivește pe ambele ecraneVerificați potrivirea exactă a transitionName în XML/cod
FPS < 40 pe AndroidElementul partajat conține o ierarhie View complexăFaceți partajat doar ImageView, nu întregul CardView
iOS: artefacte snapshotrenderInContext pe strat cu măști/animațiiFolosiți drawHierarchy(in:afterScreenUpdates: true)
Compose: elementul nu apareSharedTransitionLayout nu învelește ambele ecraneSharedTransitionLayout trebuie să fie părinte pentru ambele
kotlin
// Optimizare: Placeholder pentru imagine în shared transition
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide

"> Încărcarea preview înainte de navigare
Glide.with(context)
    .load(imageUrl)
    .override(300, 300) // Limitarea dimensiunii pentru shared transition
    .placeholder(R.drawable.placeholder)
    .into(imageView)

> Încărcare întârziată full-resolution după tranziție
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
    Glide.with(context).load(imageUrl).into(detailImage)
}

// iOS: optimizarea snapshot cu drawHierarchy
// snapshotView(afterScreenUpdates: false) — mai rapid, ignoră actualizările în așteptare
// Pentru View-uri complexe: drawHierarchy(in: view.bounds, afterScreenUpdates: true)

Cele mai bune practici: nu adăugați mai mult de 3 elemente partajate per tranziție — fiecare element suplimentar crește încărcarea de randare. Pentru liste (RecyclerView/UICollectionView) faceți elementul partajat pe elementul clicat, nu pe întreaga listă. Pentru carduri cu colțuri rotunjite folosiți clipToOutline pe Android sau cornerRadius în snapshot pe iOS. Dacă elementul partajat pe al doilea ecran se află într-un ScrollView, așteptați finalizarea layout-ului (view.doOnPreDraw) înainte de a începe animația.

Întrebări frecvente

Câte elemente partajate pot fi animate într-o singură tranziție?

Se recomandă nu mai mult de 3 elemente partajate per tranziție. Fiecare element suplimentar crește încărcarea de randare și poate cauza scăderea FPS. Optim: 1 imagine + 1 text (titlu). Pentru Android, fiecare element se adaugă prin addSharedElement(view, transitionName). Pentru iOS, fiecare snapshot se adaugă separat în containerView.

Shared Element Transition nu funcționează cu RecyclerView — ce să fac?

Asigurați-vă că transitionName este setat dinamic în onBindViewHolder: holder.imageView.transitionName = «image_${position}». Pentru FragmentTransaction.addSharedElement transmiteți View-ul specific din holder. Pentru ActivityOptions folosiți Pair.create(holder.imageView, holder.imageView.transitionName). Amânați shared transition până la finalizarea layout-ului prin postponeEnterTransition() și startPostponedEnterTransition().

Cum se face Shared Element Transition cu schimbarea culorii de fundal?

Pe Android: adăugați ChangeBackground în TransitionSet sau folosiți un Transition personalizat care suprascrie captureStartValues și captureEndValues. Pe iOS: animați backgroundColor al snapshot-ului sau UIViewPropertyAnimator cu schimbare de culoare. În Jetpack Compose: Modifier.sharedElement animează automat background-ul prin Color transition. Pentru carduri, animați și elevation — de la 2dp pe listă la 8dp pe ecranul de detalii.

Funcționează Shared Element pe Android cu Jetpack Navigation?

Da, cu Navigation Component Shared Element este suportat prin NavOptions: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). Folosiți FragmentNavigatorExtras pentru a transmite elemente partajate: FragmentNavigatorExtras(view to transitionName). Pentru Navigation Compose folosiți Modifier.sharedElement din SharedTransitionScope (Compose 1.7+).

Cum se depanează Shared Element Transition?

Pe Android activați Transition Debugging: adb shell setprop debug.transition 1 — în logcat vor apărea mesaje despre capturarea și animarea elementelor partajate. Activați «Show layout bounds» în Developer Options pentru a verifica limitele înainte/după. Pe iOS folosiți Debug View Hierarchy pentru a verifica snapshot-ul în containerView. Pentru monitorizarea FPS folosiți Profile GPU Rendering (Android) sau Core Animation instrument (iOS).

Rezumat

  • Shared Element Transition — animația elementului comun între ecrane cu păstrarea contextului vizual.
  • Android: FragmentTransaction.addSharedElement sau ActivityOptions.makeSceneTransitionAnimation.
  • iOS: implementare personalizată prin UIViewControllerAnimatedTransitioning + snapshot + PropertyAnimator.
  • Jetpack Compose: Modifier.sharedElement din SharedTransitionScope (Compose 1.7+).
  • Maximum 3 elemente partajate per tranziție pentru a evita scăderea FPS.
  • Pentru ImageView cu scaleType diferit folosiți ChangeImageTransform (Android) sau snapshot (iOS).
  • Biblioteca Hero (iOS) — soluție terță parte cu hero.id, analogă transitionName în Android.

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și