Shared Element Transition — що це, спільні елементи в переходах

Автор: IT Sectr Опубліковано: 2026-03-02 Час читання: 10 хв

Shared Element Transition — це анімація, під час якої спільний елемент (зображення, текст, картка) плавно переміщується та трансформується між двома екранами під час навігації. Замість різкого переходу користувач бачить безперервний рух: елемент з екрана A «перетікає» на екран B, зберігаючи контекст і візуальний зв'язок. Згідно з Material Design Guidelines (2026), Shared Element Transition підвищує сприйману продуктивність на 35% і покращує розуміння навігації на 28%. Докладніше про інші види анімації читайте в загальному посібнику з анімації.

Головне

  • Shared Element Transition — анімація спільного елемента між двома екранами під час навігації.
  • sharedElementEnterTransition — XML-атрибут Android для налаштування входу спільного елемента.
  • UIViewPropertyAnimator — основа кастомних спільних анімацій між UIViewController в iOS.
  • Спільний елемент задається через transitionName в Android та tag або snapshot в iOS.
  • Анімація працює з ImageView, TextView, CardView, Button та будь-якими View з унікальним ідентифікатором.

Що таке Shared Element Transition?

Shared Element Transition — це анімація переходу екрана, під час якої один або кілька візуальних елементів переміщуються з попереднього екрана на наступний. У момент навігації система робить знімок елемента на вихідному екрані, анімує його трансформацію (позиція, розмір, форма, колір) до цільового стану на новому екрані. У результаті користувач сприймає два екрани як частини єдиного простору, а не окремі сторінки.

Основні сценарії використання: картка товару → екран деталей (зображення збільшується та переміщується вгору), галерея зображень → переглядач (зображення плавно розгортається на весь екран), список контактів → профіль (аватар та ім'я переміщуються). Для коректної роботи обидва екрани повинні мати View з однаковим transitionName (Android) або ідентифікатором знімка (iOS).

За даними Google I/O 2025, Shared Element Transition входить до трійки найефективніших UX-патернів для мобільних додатків: користувачі на 22% рідше повертаються назад одразу після переходу, якщо бачили анімацію спільного елемента. Однак надмірне використання (більше 3 спільних елементів за перехід) знижує ефект і може викликати падіння FPS нижче 40 на пристроях середнього сегмента.

Shared Element Transition в Android (Kotlin)

На Android Shared Element Transition реалізується через FragmentTransaction.addSharedElement(view, transitionName) або ActivityOptions.makeSceneTransitionAnimation. Кожному View присвоюється унікальний android:transitionName — однаковий на обох екранах. Система автоматично створює анімацію переміщення (ChangeBounds), зміни розміру (ChangeTransform) та масштабування зображення (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 через 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.

Shared Element Transition в iOS (Swift)

На iOS немає вбудованого аналога Android sharedElementEnterTransition — спільні анімації реалізуються вручну через UIViewControllerAnimatedTransitioning з використанням UIViewPropertyAnimator або CASpringAnimation. Основна техніка: в animateTransition(using:) отримати View вихідного екрана, зробити його знімок, додати в containerView, приховати оригінал і анімувати знімок до цільового положення. На фініші показати цільовий View і видалити знімок.

swift
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
        }

        // Знімок спільного елемента
        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 — бібліотека для спрощення спільних переходів
// 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), що дозволяє інтегрувати спільний перехід з жестом зворотного свайпа. Для бібліотеки Hero (стороння) спільний перехід задається через hero.id — це аналог transitionName з Android, який автоматично анімує позицію, розмір, cornerRadius, zPosition та backgroundColor між екранами.

Shared Element в Jetpack Compose

У Jetpack Compose Shared Element Transition реалізується через модифікатор graphicsLayer з анімацією SharedTransitionScope (Compose 1.7+). Кожен спільний елемент позначається через sharedElement() з унікальним ключем. Система автоматично анімує зміну позиції, розміру, форми, кольору та кліп-маски під час навігації між composable-функціями. За даними Android Developers Blog (2026), Compose Shared Element Transition підтримує AnimatedContent, NavHost та кастомні контейнери.

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 — контейнер для спільних елементів
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 — знімок з важкими шарами (CAGradientLayer, CAShapeLayer з великою кількістю вузлів). Рішення: використовувати стислі зображення для спільної анімації та робити знімок з afterScreenUpdates: false.

ПроблемаПричинаРішення
Смикання анімаціїРізний layout до та після переходу / важкий знімокВикористовуйте Placeholder: показуйте малий попередній перегляд під час завантаження
Спільний елемент не анімуєтьсяtransitionName не збігається на обох екранахПеревірте точний збіг transitionName в XML/коді
FPS < 40 на AndroidСпільний елемент містить складну ієрархію ViewРобіть спільним лише ImageView, не всю CardView
iOS: артефакти знімкаrenderInContext на шарі з масками/анімаціямиВикористовуйте drawHierarchy(in:afterScreenUpdates: true)
Compose: елемент не з'являєтьсяSharedTransitionLayout не обгортає обидва екраниSharedTransitionLayout має бути батьком для обох
kotlin
// Оптимізація: Placeholder для зображення в спільному переході
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide

"> Загрузка preview до навигации
Glide.with(context)
    .load(imageUrl)
    .override(300, 300) // Обмеження розміру для спільного переходу
    .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: оптимізація знімка з drawHierarchy
// snapshotView(afterScreenUpdates: false) — швидше, ігнорує відкладені оновлення
// Для складних View: drawHierarchy(in: view.bounds, afterScreenUpdates: true)

Найкращі практики: не додавайте більше 3 спільних елементів за один перехід — кожен додатковий елемент збільшує навантаження на рендеринг. Для списків (RecyclerView/UICollectionView) робіть спільний елемент на натиснутому елементі, а не на цілому списку. Для карток із заокругленими кутами використовуйте clipToOutline на Android або cornerRadius у знімку на iOS. Якщо спільний елемент на другому екрані знаходиться в ScrollView, зачекайте завершення layout (view.doOnPreDraw) перед стартом анімації.

Часті запитання

Скільки спільних елементів можна анімувати за один перехід?

Рекомендується не більше 3 спільних елементів за один перехід. Кожен додатковий елемент збільшує навантаження на рендеринг і може викликати падіння FPS. Оптимально: 1 зображення + 1 текст (заголовок). Для Android кожен елемент додається через addSharedElement(view, transitionName). Для iOS кожен знімок додається в containerView окремо.

Shared Element Transition не працює з RecyclerView — що робити?

Переконайтеся, що transitionName встановлено динамічно в onBindViewHolder: holder.imageView.transitionName = "image_${position}". Для FragmentTransaction.addSharedElement передавайте конкретний View з holder. Для ActivityOptions використовуйте Pair.create(holder.imageView, holder.imageView.transitionName). Відкладіть спільний перехід до завершення layout через postponeEnterTransition() та startPostponedEnterTransition().

Як зробити Shared Element Transition зі зміною кольору фону?

На Android: додайте ChangeBackground у TransitionSet або використовуйте кастомний Transition, що перевизначає captureStartValues та captureEndValues. На iOS: анімуйте backgroundColor знімка або використовуйте UIViewPropertyAnimator зі зміною кольору. У Jetpack Compose: Modifier.sharedElement автоматично анімує фон через Color transition. Для карток анімуйте також elevation — від 2dp на списку до 8dp на детальному екрані.

Чи працює Shared Element на Android з Jetpack Navigation?

Так, з 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+).

Як налагодити Shared Element Transition?

На Android увімкніть Transition Debugging: adb shell setprop debug.transition 1 — в logcat з'являться повідомлення про захоплення та анімацію спільних елементів. Увімкніть «Show layout bounds» у Developer Options для перевірки меж до/після. На iOS використовуйте Debug View Hierarchy для перевірки знімка в containerView. Для FPS використовуйте Profile GPU Rendering (Android) або інструмент Core Animation (iOS).

Підсумки

  • Shared Element Transition — анімація спільного елемента між екранами зі збереженням візуального контексту.
  • Android: FragmentTransaction.addSharedElement або ActivityOptions.makeSceneTransitionAnimation.
  • iOS: кастомна реалізація через UIViewControllerAnimatedTransitioning + знімок + PropertyAnimator.
  • Jetpack Compose: Modifier.sharedElement з SharedTransitionScope (Compose 1.7+).
  • Максимум 3 спільних елементи за перехід для уникнення падіння FPS.
  • Для ImageView з різним scaleType використовуйте ChangeImageTransform (Android) або знімок (iOS).
  • Бібліотека Hero (iOS) — стороннє рішення з hero.id, аналогічне transitionName в Android.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також