Shared Element Transition — animace, při které se sdílený prvek (obrázek, text, karta) plynule pohybuje a transformuje mezi dvěma obrazovkami během navigace. Místo ostrého přechodu uživatel vidí nepřetržitý pohyb: prvek z obrazovky A «přetéká» na obrazovku B, přičemž zachovává kontext a vizuální spojení. Podle Material Design Guidelines (2026) zvyšuje Shared Element Transition vnímaný výkon o 35% a zlepšuje porozumění navigaci o 28%. Více o dalších typech animací si přečtěte v obecném průvodci animacemi.
Hlavní body
Shared Element Transition — je animace změny obrazovky, při které se jeden nebo více vizuálních prvků «stěhuje» z předchozí obrazovky na následující. V okamžiku navigace systém pořídí snapshot prvku na zdrojové obrazovce, animuje jeho transformaci (pozice, velikost, tvar, barva) do cílového stavu na nové obrazovce. Výsledkem je, že uživatel vnímá dvě obrazovky jako části jednotného prostoru, nikoli jako samostatné stránky.
Hlavní scénáře použití: karta produktu → detailní obrazovka (obrázek se zvětší a přesune nahoru), galerie obrázků → náhled (obrázek se plynule otevře na celou obrazovku), seznam kontaktů → profil (avatar a jméno se přesunou). Pro správnou funkci musí mít obě obrazovky View se stejným transitionName (Android) nebo identifikátorem snapshot (iOS).
Podle Google I/O 2025 je Shared Element Transition jedním ze tří nejúčinnějších UX vzorů pro mobilní aplikace: uživatelé se o 22% méně často vracejí okamžitě po přechodu, pokud viděli animaci sdíleného prvku. Nadměrné používání (více než 3 sdílené prvky na přechod) však snižuje efekt a může způsobit pokles FPS pod 40 na zařízeních střední třídy.
V Android je Shared Element Transition implementován pomocí FragmentTransaction.addSharedElement(view, transitionName) nebo ActivityOptions.makeSceneTransitionAnimation. Každému View je přiřazen jedinečný android:transitionName — stejný na obou obrazovkách. Systém automaticky vytváří animaci pohybu (ChangeBounds), změny velikosti (ChangeTransform) a škálování obrázku (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 přes 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 přes 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()
// Přizpůsobení přechodu: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 400L
interpolator = FastOutSlowInInterpolator()
}
Důležité detaily Android: pro správnou animaci ImageView s různým scaleType (centerCrop → fitCenter) použijte ChangeImageTransform — automaticky animuje změnu způsobu zobrazení. Pro View se zaoblenými rohy (CardView) přidejte ChangeBounds, který animuje poloměr zaoblení přes outline. Pro animaci textu (změna velikosti písma) použijte ChangeTextTransform. Pokud sdílený prvek zmizí nebo se objeví na jedné z obrazovek — systém automaticky aplikuje Fade.
V iOS neexistuje vestavěný ekvivalent Android sharedElementEnterTransition — sdílené animace se implementují ručně pomocí UIViewControllerAnimatedTransitioning s UIViewPropertyAnimator nebo CASpringAnimation. Hlavní technika: v animateTransition(using:) vezměte View zdrojové obrazovky, vytvořte jeho snapshot, přidejte do containerView, skryjte originál a animujte snapshot na cílovou pozici. Na konci zobrazte cílové View a odstraňte snapshot.
import UIKit
"> Vlastní animátor 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 sdíleného prvku
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 — knihovna pro zjednodušení shared transitions
// heroID souhlasí na obou obrazovkách, analogicky k transitionName v Android
import Hero
imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"
UIViewPropertyAnimator — preferovaný způsob pro sdílené animace na iOS 10+. Na rozdíl od UIView.animate podporuje PropertyAnimator interaktivní ovládání (fractionComplete), což umožňuje integraci shared transition s gestem swipe-back. Pro knihovnu Hero (třetí strana) se shared transition nastavuje přes hero.id — to je analogie transitionName z Android, která automaticky animuje pozici, velikost, cornerRadius, zPosition a backgroundColor mezi obrazovkami.
V Jetpack Compose je Shared Element Transition implementován pomocí modifikátoru graphicsLayer s animací SharedTransitionScope (Compose 1.7+). Každý sdílený prvek je označen pomocí sharedElement() s jedinečným klíčem. Systém automaticky animuje změnu pozice, velikosti, tvaru, barvy a ořezové masky během navigace mezi composable funkcemi. Podle Android Developers Blog (2026) podporuje Compose Shared Element Transition AnimatedContent, NavHost a vlastní kontejnery.
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 — kontejner pro sdílené prvky
SharedTransitionLayout {
val sharedTransitionState = remember { SharedTransitionState() }
// Obrazovka A: seznam
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)
}
}
}
// Obrazovka B: detaily (zobrazí se při výběru prvku)
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 — nové API dostupné v experimentálním režimu. Nahrazuje ruční správu sdílených animací v NavHost. Klíčové možnosti: boundsTransform pro vlastní křivku animace, clipToBounds pro zaoblené rohy, zIndex pro pořadí překrývání. Pro animaci obrázků s různým contentScale použijte Modifier.sharedElement s obalem SharedContentKey. Pro podporu starších verzí Compose (< 1.7) použijte knihovnu SharedTransitionScope od Google.
Shared Element Transition může způsobovat problémy s výkonem, pokud se neberou v úvahu omezení platformy. V Android je hlavním problémem ChangeImageTransform s velkými obrázky (nad 2048 px). V iOS — snapshot s těžkými vrstvami (CAGradientLayer, CAShapeLayer s velkým počtem uzlů). Řešení: používejte komprimované obrázky pro sdílené animace a vytvářejte snapshot s afterScreenUpdates: false.
| Problém | Příčina | Řešení |
|---|---|---|
| Trhavá animace | Různý layout před a po přechodu / těžký snapshot | Použijte Placeholder: zobrazte malý náhled při načítání |
| Sdílený prvek se neanimuje | transitionName nesouhlasí na obou obrazovkách | Zkontrolujte přesnou shodu transitionName v XML/kódu |
| FPS < 40 na Android | Sdílený prvek obsahuje složitou hierarchii View | Sdílejte pouze ImageView, ne celý CardView |
| iOS: artefakty snapshot | renderInContext na vrstvě s maskami/animacemi | Použijte drawHierarchy(in:afterScreenUpdates: true) |
| Compose: prvek se nezobrazuje | SharedTransitionLayout neobaluje obě obrazovky | SharedTransitionLayout musí být rodičem pro obě |
// Optimalizace: Placeholder pro obrázek v shared transition
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> Načítání preview před navigací
Glide.with(context)
.load(imageUrl)
.override(300, 300) // Omezení velikosti pro shared transition
.placeholder(R.drawable.placeholder)
.into(imageView)
> Odložené načítání full-resolution po přechodu
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS: optimalizace snapshot pomocí drawHierarchy
// snapshotView(afterScreenUpdates: false) — rychlejší, ignoruje čekající aktualizace
// Pro složitá View: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
Nejlepší postupy: nepřidávejte více než 3 sdílené prvky na přechod — každý další prvek zvyšuje zátěž vykreslování. Pro seznamy (RecyclerView/UICollectionView) vytvářejte sdílený prvek na kliknutou položku, ne na celý seznam. Pro karty se zaoblenými rohy použijte clipToOutline v Android nebo cornerRadius v snapshot v iOS. Pokud je sdílený prvek na druhé obrazovce v ScrollView, počkejte na dokončení layoutu (view.doOnPreDraw) před spuštěním animace.
Často kladené otázky
Doporučuje se ne více než 3 sdílené prvky na přechod. Každý další prvek zvyšuje zátěž vykreslování a může způsobit pokles FPS. Optimálně: 1 obrázek + 1 text (nadpis). Pro Android se každý prvek přidává přes addSharedElement(view, transitionName). Pro iOS se každý snapshot přidává do containerView samostatně.
Ujistěte se, že transitionName je nastaven dynamicky v onBindViewHolder: holder.imageView.transitionName = «image_${position}». Pro FragmentTransaction.addSharedElement předejte konkrétní View z holderu. Pro ActivityOptions použijte Pair.create(holder.imageView, holder.imageView.transitionName). Odložte shared transition do dokončení layoutu pomocí postponeEnterTransition() a startPostponedEnterTransition().
V Android: přidejte ChangeBackground do TransitionSet nebo použijte vlastní Transition přepisující captureStartValues a captureEndValues. V iOS: animujte backgroundColor snapshot nebo UIViewPropertyAnimator se změnou barvy. V Jetpack Compose: Modifier.sharedElement automaticky animuje background přes Color transition. Pro karty animujte také elevation — od 2dp na seznamu do 8dp na detailní obrazovce.
Ano, s Navigation Component je Shared Element podporován přes NavOptions: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). Použijte FragmentNavigatorExtras pro předávání sdílených prvků: FragmentNavigatorExtras(view to transitionName). Pro Navigation Compose použijte Modifier.sharedElement z SharedTransitionScope (Compose 1.7+).
V Android zapněte Transition Debugging: adb shell setprop debug.transition 1 — v logcat se objeví zprávy o zachycení a animaci sdílených prvků. Zapněte «Show layout bounds» v Developer Options pro kontrolu hranic před/po. V iOS použijte Debug View Hierarchy pro kontrolu snapshot v containerView. Pro monitorování FPS použijte Profile GPU Rendering (Android) nebo Core Animation instrument (iOS).
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také