Shared Element Transition — анимација при којој се заједнички елемент (слика, текст, картица) глатко помера и трансформише између два екрана током навигације. Уместо наглог прелаза, корисник види непрекидно кретање: елемент са екрана А «прелази» на екран Б, чувајући контекст и визуелну везу. Према Material Design Guidelines (2026), Shared Element Transition повећава перципиране перформансе за 35% и побољшава разумевање навигације за 28%. Више о другим врстама анимације прочитајте у општем водичу за анимацију.
Главно
Shared Element Transition — је анимација промене екрана у којој се један или више визуелних елемената «пресељава» са претходног екрана на следећи. У тренутку навигације систем прави snapshot елемента на изворном екрану, анимира његову трансформацију (позиција, величина, облик, боја) до циљног стања на новом екрану. Као резултат, корисник доживљава два екрана као делове јединственог простора, а не као одвојене странице.
Главни сценарији коришћења: картица производа → детаљни екран (слика се увећава и прелази на врх), галерија слика → преглед (слика се глатко отвара преко целог екрана), листа контаката → профил (аватар и име се померају). За исправан рад оба екрана морају имати View са истим transitionName (Android) или идентификатором snapshot (iOS).
Према Google I/O 2025, Shared Element Transition је међу три најефикаснија UX обрасца за мобилне апликације: корисници се 22% ређе враћају одмах након прелаза ако су видели анимацију заједничког елемента. Међутим, прекомерна употреба (више од 3 shared елемента по прелазу) смањује ефекат и може изазвати пад FPS испод 40 на уређајима средњег сегмента.
На Android-у Shared Element Transition се реализује кроз FragmentTransaction.addSharedElement(view, transitionName) или ActivityOptions.makeSceneTransitionAnimation. Сваком View-у се додељује јединствени android:transitionName — исти на оба екрана. Систем аутоматски креира анимацију померања (ChangeBounds), промене величине (ChangeTransform) и скалирања слике (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 кроз 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 кроз 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()
// Прилагођавање прелаза: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 400L
interpolator = FastOutSlowInInterpolator()
}
Важни детаљи Android: за исправну анимацију ImageView са различитим scaleType (centerCrop → fitCenter) користите ChangeImageTransform — он аутоматски анимира промену начина приказа. За View са заобљеним угловима (CardView) додајте ChangeBounds који анимира радијус заобљења кроз outline. За анимацију текста (промена величине фонта) користите ChangeTextTransform. Ако shared елемент нестаје или се појављује на једном од екрана — систем аутоматски примењује Fade.
На iOS-у не постоји уграђени аналог Android-овог sharedElementEnterTransition — shared-анимације се имплементирају ручно кроз UIViewControllerAnimatedTransitioning коришћењем UIViewPropertyAnimator или CASpringAnimation. Главна техника: у animateTransition(using:) узмите View изворног екрана, направите његов snapshot, додајте у containerView, сакријте оригинал и анимирајте snapshot до циљне позиције. На крају прикажите циљни View и уклоните snapshot.
import UIKit
"> Кастомный аниматор 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 заједничког елемента
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 — библиотека за поједностављење shared transitions
// heroID се поклапа на оба екрана, аналогно transitionName у Android-у
import Hero
imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"
UIViewPropertyAnimator — пожељан начин за shared-анимације на iOS 10+. За разлику од UIView.animate, PropertyAnimator подржава интерактивну контролу (fractionComplete), што омогућава интеграцију shared transition-а са гестом swipe-back. За библиотеку Hero (трећа страна) shared transition се поставља кроз hero.id — то је аналог transitionName из Android-а, који аутоматски анимира позицију, величину, cornerRadius, zPosition и backgroundColor између екрана.
У Jetpack Compose-у Shared Element Transition се реализује кроз модификатор graphicsLayer са анимацијом SharedTransitionScope (Compose 1.7+). Сваки shared елемент се означава кроз sharedElement() са јединственим кључем. Систем аутоматски анимира промену позиције, величине, облика, боје и маске за исецање током навигације између composable функција. Према Android Developers Blog (2026), Compose Shared Element Transition подржава AnimatedContent, NavHost и прилагођене контејнере.
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 — контејнер за shared елементе
SharedTransitionLayout {
val sharedTransitionState = remember { SharedTransitionState() }
// Екран А: листа
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)
}
}
}
// Екран Б: детаљи (приказује се при избору елемента)
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 доступан у експерименталном режиму. Замењује ручно управљање shared-анимацијама у NavHost-у. Кључне могућности: boundsTransform за прилагођену криву анимације, clipToBounds за заобљене углове, zIndex за редослед слојева. За анимацију слика са различитим contentScale користите Modifier.sharedElement са wrapper-ом SharedContentKey. За подршку старијим верзијама Compose-а (< 1.7) користите библиотеку SharedTransitionScope од Google-а.
Shared Element Transition може изазвати проблеме са перформансама ако се не узму у обзир ограничења платформе. На Android-у главни проблем је ChangeImageTransform са великим сликама (изнад 2048 px). На iOS-у — snapshot са тешким слојевима (CAGradientLayer, CAShapeLayer са великим бројем чворова). Решење: користите компресоване слике за shared-анимацију и правите snapshot са afterScreenUpdates: false.
| Проблем | Узрок | Решење |
|---|---|---|
| Трзање анимације | Различит layout пре и после прелаза / тежак snapshot | Користите Placeholder: прикажите мали преглед током учитавања |
| Shared елемент се не анимира | transitionName се не поклапа на оба екрана | Проверите тачно поклапање transitionName у XML/коду |
| FPS < 40 на Android-у | Shared елемент садржи сложену хијерархију View-ова | Правите shared само ImageView, не цео CardView |
| iOS: артефакти snapshot | renderInContext на слоју са маскама/анимацијама | Користите drawHierarchy(in:afterScreenUpdates: true) |
| Compose: елемент се не појављује | SharedTransitionLayout не обухвата оба екрана | SharedTransitionLayout мора бити родитељ за оба |
// Оптимизација: Placeholder за слику у shared transition
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> Загрузка preview до навигации
Glide.with(context)
.load(imageUrl)
.override(300, 300) // Ограничење величине за shared transition
.placeholder(R.drawable.placeholder)
.into(imageView)
> Отложенная загрузка full-resolution после перехода
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS: оптимизација snapshot-а са drawHierarchy
// snapshotView(afterScreenUpdates: false) — брже, игнорише надолазеће промене
// За сложене View-ове: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
Најбоље праксе: не додајте више од 3 shared елемента по прелазу — сваки додатни елемент повећава оптерећење рендеровања. За листе (RecyclerView/UICollectionView) радите shared елемент на кликнути item, а не на целу листу. За картице са заобљеним угловима користите clipToOutline на Android-у или cornerRadius у snapshot-у на iOS-у. Ако се shared елемент на другом екрану налази у ScrollView-у, сачекајте завршетак layout-а (view.doOnPreDraw) пре почетка анимације.
Често постављана питања
Препоручује се не више од 3 shared елемента по прелазу. Сваки додатни елемент повећава оптерећење рендеровања и може изазвати пад FPS-а. Оптимално: 1 слика + 1 текст (наслов). За Android сваки елемент се додаје кроз addSharedElement(view, transitionName). За iOS сваки snapshot се додаје у containerView посебно.
Уверите се да је transitionName постављен динамички у onBindViewHolder: holder.imageView.transitionName = «image_${position}». За FragmentTransaction.addSharedElement преносите конкретан View из holder-а. За ActivityOptions користите Pair.create(holder.imageView, holder.imageView.transitionName). Одложите shared transition до завршетка layout-а кроз postponeEnterTransition() и startPostponedEnterTransition().
На Android-у: додајте ChangeBackground у TransitionSet или користите прилагођени Transition који преписује captureStartValues и captureEndValues. На iOS-у: анимирајте backgroundColor snapshot-а или UIViewPropertyAnimator са променом боје. У Jetpack Compose-у: Modifier.sharedElement аутоматски анимира background кроз Color transition. За картице анимирајте и elevation — од 2dp на листи до 8dp на детаљном екрану.
Да, са Navigation Component Shared Element је подржан кроз NavOptions: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). Користите FragmentNavigatorExtras за пренос shared елемената: FragmentNavigatorExtras(view to transitionName). За Navigation Compose користите Modifier.sharedElement из SharedTransitionScope (Compose 1.7+).
На Android-у укључите Transition Debugging: adb shell setprop debug.transition 1 — у logcat-у ће се појавити поруке о хватању и анимацији shared елемената. Укључите «Show layout bounds» у Developer Options за проверу граница пре/после. На iOS-у користите Debug View Hierarchy за проверу snapshot-а у containerView-у. За праћење FPS-а користите Profile GPU Rendering (Android) или Core Animation instrument (iOS).
Закључак
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође