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 — основа кастомных shared-анимаций между UIViewController в iOS.
  • Общий элемент задаётся через transitionName в Android и tag или snapshot в iOS.
  • Анимация работает с ImageView, TextView, CardView, Button и любыми View с уникальным идентификатором.

Что такое Shared Element Transition?

Shared Element Transition — это анимация смены экранов, где один или несколько визуальных элементов «переезжают» с предыдущего экрана на следующий. В момент навигации система делает снапшот элемента на исходном экране, анимирует его трансформацию (позиция, размер, форма, цвет) к целевому состоянию на новом экране. В результате пользователь воспринимает два экрана как части единого пространства, а не как отдельные страницы.

Основные сценарии использования: карточка товара → детальный экран (изображение увеличивается и переходит наверх), галерея изображений → просмотр (картинка плавно раскрывается на весь экран), список контактов → профиль (аватар и имя перемещаются). Для корректной работы оба экрана должны иметь View с одинаковым transitionName (Android) или snapshot-идентификатором (iOS).

По данным Google I/O 2025, Shared Element Transition входит в тройку самых эффективных UX-паттернов для мобильных приложений: пользователи на 22% реже возвращаются назад сразу после перехода, если видели анимацию общего элемента. Однако избыточное использование (более 3 shared элементов за переход) снижает эффект и может вызвать падение 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. Если shared элемент исчезает или появляется на одном из экранов — система автоматически применит Fade.

Shared Element Transition в iOS (Swift)

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

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
        }

        // 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 между экранами.

Shared Element в Jetpack Compose

В Jetpack Compose Shared Element Transition реализуется через модификатор graphicsLayer с анимацией SharedTransitionScope (Compose 1.7+). Каждый shared элемент помечается через sharedElement() с уникальным ключом. Система автоматически анимирует изменение позиции, размера, формы, цвета и clip-маски при навигации между 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 — контейнер для shared элементов
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, доступный в экспериментальном режиме. Он заменяет ручное управление shared-анимациями в NavHost. Ключевые возможности: boundsTransform для кастомной кривой анимации, clipToBounds для закруглённых углов, zIndex для порядка наложения. Для анимации изображений с разным contentScale используйте Modifier.sharedElement с обёрткой SharedContentKey. Для поддержки старых версий Compose (< 1.7) используйте библиотеку SharedTransitionScope от Google.

Оптимизация и типовые проблемы

Shared Element Transition может вызывать проблемы с производительностью, если не учитывать ограничения платформы. На Android основная проблема — ChangeImageTransform с большими изображениями (выше 2048 px). На iOS — snapshot с тяжёлыми слоями (CAGradientLayer, CAShapeLayer с большим количеством узлов). Решение: использовать сжатые изображения для shared-анимации и делать snapshot с afterScreenUpdates: false.

ПроблемаПричинаРешение
Дёрганье анимацииРазный layout до и после перехода / тяжёлый snapshotИспользуйте Placeholder: отображайте small preview при загрузке
Shared элемент не анимируетсяtransitionName не совпадает на обоих экранахПроверьте точное совпадение transitionName в XML/коде
FPS < 40 на AndroidShared элемент содержит сложную иерархию ViewДелайте shared только ImageView, не всю CardView
iOS: артефакты snapshotrenderInContext на слое с масками/анимациямиИспользуйте drawHierarchy(in:afterScreenUpdates: true)
Compose: элемент не появляетсяSharedTransitionLayout не оборачивает оба экранаSharedTransitionLayout должен быть родителем для обоих
kotlin
// Оптимизация: 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) — быстрее игнорирует pending updates
// Для сложных View: drawHierarchy(in: view.bounds, afterScreenUpdates: true)

Лучшие практики: не добавляйте более 3 shared элементов за один переход — каждый дополнительный элемент увеличивает нагрузку на рендеринг. Для списков (RecyclerView/UICollectionView) делайте shared элемент на кликнутый item, а не на весь список. Для карточек с закруглёнными углами используйте clipToOutline на Android или cornerRadius в snapshot на iOS. Если shared элемент на втором экране находится в ScrollView, дождитесь завершения layout (view.doOnPreDraw) перед стартом анимации.

Часто задаваемые вопросы

Сколько shared элементов можно анимировать за один переход?

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

Shared Element Transition не работает с RecyclerView — что делать?

Убедитесь, что transitionName установлен динамически в onBindViewHolder: holder.imageView.transitionName = «image_${position}». Для FragmentTransaction.addSharedElement передавайте конкретный View из holder. Для ActivityOptions используйте Pair.create(holder.imageView, holder.imageView.transitionName). Отложите shared transition до завершения layout через postponeEnterTransition() и startPostponedEnterTransition().

Как сделать Shared Element Transition с изменением цвета фона?

На Android: добавьте ChangeBackground в TransitionSet или используйте кастомный Transition, переопределяющий captureStartValues и captureEndValues. На iOS: анимируйте backgroundColor snapshot или UIViewPropertyAnimator с изменением цвета. В Jetpack Compose: Modifier.sharedElement автоматически анимирует background через 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 для передачи shared элементов: 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 появятся сообщения о захвате и анимации shared элементов. Включите «Show layout bounds» в Developer Options для проверки границ до/после. На iOS используйте Debug View Hierarchy для проверки snapshot в containerView. Для FPS используйте Profile GPU Rendering (Android) или Core Animation instrument (iOS).

Итоги

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

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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