Shared Element Transition — co to jest, wspólne elementy w przejściach

Autor: IT Sectr Opublikowano: 2026-03-02 Czas czytania: 10 min

Shared Element Transition — animacja, w której wspólny element (obraz, tekst, karta) płynnie przemieszcza się i transformuje między dwoma ekranami podczas nawigacji. Zamiast ostrego przejścia użytkownik widzi ciągły ruch: element z ekranu A «przepływa» na ekran B, zachowując kontekst i wizualne powiązanie. Według Material Design Guidelines (2026), Shared Element Transition zwiększa postrzeganą wydajność o 35% i poprawia zrozumienie nawigacji o 28%. Więcej o innych rodzajach animacji przeczytasz w ogólnym przewodniku po animacji.

Najważniejsze

  • Shared Element Transition — animacja wspólnego elementu między dwoma ekranami podczas nawigacji.
  • sharedElementEnterTransition — atrybut XML Androida do konfiguracji wejścia wspólnego elementu.
  • UIViewPropertyAnimator — podstawa niestandardowych shared-animacji między UIViewController w iOS.
  • Wspólny element ustawia się przez transitionName w Androidzie i tag lub snapshot w iOS.
  • Animacja działa z ImageView, TextView, CardView, Button i dowolnymi View z unikalnym identyfikatorem.

Co to jest Shared Element Transition?

Shared Element Transition — to animacja zmiany ekranów, w której jeden lub kilka wizualnych elementów «przemieszcza się» z poprzedniego ekranu na następny. W momencie nawigacji system robi snapshot elementu na ekranie źródłowym, animuje jego transformację (pozycja, rozmiar, kształt, kolor) do docelowego stanu na nowym ekranie. W rezultacie użytkownik postrzega dwa ekrany jako części jednej przestrzeni, a nie jako osobne strony.

Główne scenariusze użycia: karta produktu → szczegółowy ekran (obraz powiększa się i przechodzi na górę), galeria obrazów → podgląd (obraz płynnie rozszerza się na cały ekran), lista kontaktów → profil (awatar i nazwa przemieszczają się). Do poprawnego działania oba ekrany muszą mieć View z tym samym transitionName (Android) lub identyfikatorem snapshot (iOS).

Według Google I/O 2025, Shared Element Transition znajduje się w trójce najskuteczniejszych wzorców UX dla aplikacji mobilnych: użytkownicy o 22% rzadziej wracają natychmiast po przejściu, jeśli widzieli animację wspólnego elementu. Jednak nadmierne użycie (więcej niż 3 shared elementy na przejście) zmniejsza efekt i może spowodować spadek FPS poniżej 40 na urządzeniach średniej klasy.

Shared Element Transition w Android (Kotlin)

W Androidzie Shared Element Transition jest realizowany przez FragmentTransaction.addSharedElement(view, transitionName) lub ActivityOptions.makeSceneTransitionAnimation. Każdemu View przypisuje się unikalny android:transitionName — taki sam na obu ekranach. System automatycznie tworzy animację przemieszczania (ChangeBounds), zmiany rozmiaru (ChangeTransform) i skalowania obrazu (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 przez 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 przez 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()

// Dostosowanie przejścia: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
    ordering = TransitionSet.ORDERING_TOGETHER
    addTransition(ChangeBounds())
    addTransition(ChangeTransform())
    addTransition(ChangeImageTransform())
    duration = 400L
    interpolator = FastOutSlowInInterpolator()
}

Ważne szczegóły Android: do poprawnej animacji ImageView z różnym scaleType (centerCrop → fitCenter) używaj ChangeImageTransform — automatycznie animuje zmianę sposobu wyświetlania. Dla View z zaokrąglonymi rogami (CardView) dodaj ChangeBounds, który animuje promień zaokrąglenia przez outline. Do animacji tekstu (zmiana rozmiaru czcionki) używaj ChangeTextTransform. Jeśli shared element znika lub pojawia się na jednym z ekranów — system automatycznie zastosuje Fade.

Shared Element Transition w iOS (Swift)

W iOS nie ma wbudowanego odpowiednika Androida sharedElementEnterTransition — shared-animacje są implementowane ręcznie przez UIViewControllerAnimatedTransitioning z użyciem UIViewPropertyAnimator lub CASpringAnimation. Główna technika: w animateTransition(using:) pobierz View ekranu źródłowego, zrób jego snapshot, dodaj do containerView, ukryj oryginał i animuj snapshot do docelowej pozycji. Po zakończeniu pokaż docelowe View i usuń snapshot.

swift
import UIKit

"> Niestandardowy animator 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 wspólnego elementu
        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 — biblioteka do upraszczania shared transitions
// heroID jest zgodny na obu ekranach, analogicznie do transitionName w Androidzie
import Hero

imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"

UIViewPropertyAnimator — preferowany sposób do shared-animacji na iOS 10+. W przeciwieństwie do UIView.animate, PropertyAnimator obsługuje interaktywną kontrolę (fractionComplete), co pozwala zintegrować shared transition z gestem swipe-back. Dla biblioteki Hero (zewnętrzna) shared transition ustawia się przez hero.id — to odpowiednik transitionName z Androida, który automatycznie animuje pozycję, rozmiar, cornerRadius, zPosition i backgroundColor między ekranami.

Shared Element w Jetpack Compose

W Jetpack Compose Shared Element Transition jest realizowany przez modyfikator graphicsLayer z animacją SharedTransitionScope (Compose 1.7+). Każdy shared element jest oznaczony przez sharedElement() z unikalnym kluczem. System automatycznie animuje zmianę pozycji, rozmiaru, kształtu, koloru i maski przycinania podczas nawigacji między funkcjami composable. Według Android Developers Blog (2026), Compose Shared Element Transition obsługuje AnimatedContent, NavHost i niestandardowe kontenery.

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 — kontener dla shared elementów
SharedTransitionLayout {
    val sharedTransitionState = remember { SharedTransitionState() }

    // Ekran A: lista
    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)
            }
        }
    }

    // Ekran B: szczegóły (pokazywane przy wyborze elementu)
    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 — nowe API dostępne w trybie eksperymentalnym. Zastępuje ręczne zarządzanie shared-animacjami w NavHost. Kluczowe możliwości: boundsTransform dla niestandardowej krzywej animacji, clipToBounds dla zaokrąglonych rogów, zIndex dla kolejności nakładania. Do animacji obrazów z różnym contentScale używaj Modifier.sharedElement z wrapperem SharedContentKey. Dla wsparcia starszych wersji Compose (< 1.7) używaj biblioteki SharedTransitionScope od Google.

Optymalizacja i typowe problemy

Shared Element Transition może powodować problemy z wydajnością, jeśli nie uwzględnia się ograniczeń platformy. W Androidzie głównym problemem jest ChangeImageTransform z dużymi obrazami (powyżej 2048 px). W iOS — snapshot z ciężkimi warstwami (CAGradientLayer, CAShapeLayer z dużą liczbą węzłów). Rozwiązanie: używaj skompresowanych obrazów do shared-animacji i rób snapshot z afterScreenUpdates: false.

ProblemPrzyczynaRozwiązanie
Szarpana animacjaRóżny layout przed i po przejściu / ciężki snapshotUżyj Placeholder: wyświetlaj mały podgląd podczas ładowania
Shared element nie animuje siętransitionName nie jest zgodny na obu ekranachSprawdź dokładne dopasowanie transitionName w XML/kodzie
FPS < 40 na AndroidzieShared element zawiera złożoną hierarchię ViewRób shared tylko dla ImageView, nie dla całej CardView
iOS: artefakty snapshotrenderInContext na warstwie z maskami/animacjamiUżyj drawHierarchy(in:afterScreenUpdates: true)
Compose: element nie pojawia sięSharedTransitionLayout nie obejmuje obu ekranówSharedTransitionLayout musi być rodzicem dla obu
kotlin
// Optymalizacja: Placeholder dla obrazu w shared transition
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide

"> Ładowanie preview przed nawigacją
Glide.with(context)
    .load(imageUrl)
    .override(300, 300) // Ograniczenie rozmiaru dla shared transition
    .placeholder(R.drawable.placeholder)
    .into(imageView)

> Opóźnione ładowanie full-resolution po przejściu
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
    Glide.with(context).load(imageUrl).into(detailImage)
}

// iOS: optymalizacja snapshot z drawHierarchy
// snapshotView(afterScreenUpdates: false) — szybciej ignoruje oczekujące aktualizacje
// Dla złożonych View: drawHierarchy(in: view.bounds, afterScreenUpdates: true)

Najlepsze praktyki: nie dodawaj więcej niż 3 shared elementów na jedno przejście — każdy dodatkowy element zwiększa obciążenie renderowania. Dla list (RecyclerView/UICollectionView) rób shared element na kliknięty item, a nie na całą listę. Dla kart z zaokrąglonymi rogami używaj clipToOutline w Androidzie lub cornerRadius w snapshot na iOS. Jeśli shared element na drugim ekranie znajduje się w ScrollView, poczekaj na zakończenie layout (view.doOnPreDraw) przed rozpoczęciem animacji.

Często zadawane pytania

Ile shared elementów można animować w jednym przejściu?

Zaleca się nie więcej niż 3 shared elementy na jedno przejście. Każdy dodatkowy element zwiększa obciążenie renderowania i może spowodować spadek FPS. Optymalnie: 1 obraz + 1 tekst (tytuł). Dla Androida każdy element dodaje się przez addSharedElement(view, transitionName). Dla iOS każdy snapshot dodaje się do containerView osobno.

Shared Element Transition nie działa z RecyclerView — co robić?

Upewnij się, że transitionName jest ustawiony dynamicznie w onBindViewHolder: holder.imageView.transitionName = «image_${position}». Dla FragmentTransaction.addSharedElement przekazuj konkretny View z holder. Dla ActivityOptions używaj Pair.create(holder.imageView, holder.imageView.transitionName). Odłóż shared transition do zakończenia layout przez postponeEnterTransition() i startPostponedEnterTransition().

Jak zrobić Shared Element Transition ze zmianą koloru tła?

W Androidzie: dodaj ChangeBackground do TransitionSet lub użyj niestandardowego Transition, nadpisującego captureStartValues i captureEndValues. W iOS: animuj backgroundColor snapshot lub UIViewPropertyAnimator ze zmianą koloru. W Jetpack Compose: Modifier.sharedElement automatycznie animuje background przez Color transition. Dla kart animuj również elevation — od 2dp na liście do 8dp na ekranie szczegółów.

Czy Shared Element działa na Androidzie z Jetpack Navigation?

Tak, z Navigation Component Shared Element jest obsługiwany przez NavOptions: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). Używaj FragmentNavigatorExtras do przekazywania shared elementów: FragmentNavigatorExtras(view to transitionName). Dla Navigation Compose używaj Modifier.sharedElement z SharedTransitionScope (Compose 1.7+).

Jak debugować Shared Element Transition?

W Androidzie włącz Transition Debugging: adb shell setprop debug.transition 1 — w logcat pojawią się komunikaty o przechwytywaniu i animacji shared elementów. Włącz «Show layout bounds» w Developer Options, aby sprawdzić granice przed/po. W iOS używaj Debug View Hierarchy do sprawdzenia snapshot w containerView. Do monitorowania FPS używaj Profile GPU Rendering (Android) lub Core Animation instrument (iOS).

Podsumowanie

  • Shared Element Transition — animacja wspólnego elementu między ekranami z zachowaniem kontekstu wizualnego.
  • Android: FragmentTransaction.addSharedElement lub ActivityOptions.makeSceneTransitionAnimation.
  • iOS: niestandardowa implementacja przez UIViewControllerAnimatedTransitioning + snapshot + PropertyAnimator.
  • Jetpack Compose: Modifier.sharedElement z SharedTransitionScope (Compose 1.7+).
  • Maksymalnie 3 shared elementy na przejście, aby uniknąć spadku FPS.
  • Dla ImageView z różnym scaleType używaj ChangeImageTransform (Android) lub snapshot (iOS).
  • Biblioteka Hero (iOS) — zewnętrzne rozwiązanie z hero.id, analogiczne do transitionName w Androidzie.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również