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 — 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.
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).
<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 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.
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.
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.
Î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.
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.
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 greu | Folosiți Placeholder: afișați o previzualizare mică la încărcare |
| Elementul partajat nu se animează | transitionName nu se potrivește pe ambele ecrane | Verificați potrivirea exactă a transitionName în XML/cod |
| FPS < 40 pe Android | Elementul partajat conține o ierarhie View complexă | Faceți partajat doar ImageView, nu întregul CardView |
| iOS: artefacte snapshot | renderInContext pe strat cu măști/animații | Folosiți drawHierarchy(in:afterScreenUpdates: true) |
| Compose: elementul nu apare | SharedTransitionLayout nu învelește ambele ecrane | SharedTransitionLayout trebuie să fie părinte pentru ambele |
// 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
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.
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().
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.
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+).
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
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.
Citiți și