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 споделени елемента на преход) обаче намалява ефекта и може да доведе до спад на 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. Ако споделеният елемент изчезва или се появява на един от екраните — системата автоматично прилага Fade.
В iOS няма вграден аналог на Android sharedElementEnterTransition — споделените анимации се имплементират ръчно чрез 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 — предпочитаният начин за споделени анимации на 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+). Всеки споделен елемент се маркира чрез 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 — контейнер за споделени елементи
SharedTransitionLayout {
val sharedTransitionState = remember { SharedTransitionState() }
// Екран A: списък
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)
}
}
}
// Екран B: детайли (показва се при избор на елемент)
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, достъпно в експериментален режим. Заменя ръчното управление на споделени анимации в NavHost. Ключови възможности: boundsTransform за персонализирана крива на анимация, clipToBounds за заоблени ъгли, zIndex за ред на наслагване. За анимация на изображения с различен contentScale използвайте Modifier.sharedElement с обвивка SharedContentKey. За поддръжка на по-стари версии на Compose (< 1.7) използвайте библиотеката SharedTransitionScope от Google.
Shared Element Transition може да причини проблеми с производителността, ако не се вземат предвид ограниченията на платформата. В Android основният проблем е ChangeImageTransform с големи изображения (над 2048 px). В iOS — snapshot с тежки слоеве (CAGradientLayer, CAShapeLayer с много възли). Решение: използвайте компресирани изображения за споделени анимации и правете snapshot с afterScreenUpdates: false.
| Проблем | Причина | Решение |
|---|---|---|
| Прекъсната анимация | Различен layout преди и след прехода / тежък snapshot | Използвайте Placeholder: показвайте малък преглед при зареждане |
| Споделеният елемент не се анимира | transitionName не съвпада и на двата екрана | Проверете точното съвпадение на transitionName в XML/код |
| FPS < 40 на Android | Споделеният елемент съдържа сложна йерархия от 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 споделени елемента на преход — всеки допълнителен елемент увеличава натоварването на рендирането. За списъци (RecyclerView/UICollectionView) правете споделения елемент върху кликнатия елемент, а не върху целия списък. За карти със заоблени ъгли използвайте clipToOutline в Android или cornerRadius в snapshot в iOS. Ако споделеният елемент на втория екран е в ScrollView, изчакайте завършването на layout (view.doOnPreDraw) преди да започнете анимацията.
Често задавани въпроси
Препоръчва се не повече от 3 споделени елемента на преход. Всеки допълнителен елемент увеличава натоварването на рендирането и може да доведе до спад на 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 за предаване на споделени елементи: FragmentNavigatorExtras(view to transitionName). За Navigation Compose използвайте Modifier.sharedElement от SharedTransitionScope (Compose 1.7+).
В Android включете Transition Debugging: adb shell setprop debug.transition 1 — в logcat ще се появят съобщения за заснемане и анимиране на споделени елементи. Включете «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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също