Shared Element Transition — en animering där ett delat element (bild, text, kort) smidigt flyttar sig och transformeras mellan två skärmar under navigering. Istället för en abrupt övergång ser användaren en kontinuerlig rörelse: elementet från skärm A «flyter» till skärm B och bevarar kontext och visuell koppling. Enligt Material Design Guidelines (2026) ökar Shared Element Transition upplevd prestanda med 35% och förbättrar förståelsen av navigering med 28%. Läs mer om andra typer av animering i den allmänna guiden om animering.
Huvudpunkter
Shared Element Transition — är en animering av skärmbyte där ett eller flera visuella element «flyttar» från föregående skärm till nästa. I navigeringsögonblicket tar systemet en snapshot av elementet på källskärmen, animerar dess transformation (position, storlek, form, färg) till målstatusen på den nya skärmen. Som ett resultat uppfattar användaren två skärmar som delar av ett enhetligt utrymme, inte som separata sidor.
Huvudsakliga användningsscenarier: produktkort → detaljskärm (bilden förstoras och flyttas uppåt), bildgalleri → visning (bilden öppnas smidigt till helskärm), kontaktlista → profil (avatar och namn flyttas). För korrekt funktion måste båda skärmarna ha en View med samma transitionName (Android) eller snapshot-identifierare (iOS).
Enligt Google I/O 2025 är Shared Element Transition ett av de tre mest effektiva UX-mönstren för mobila appar: användare återvänder 22% mindre ofta omedelbart efter en övergång om de såg animeringen av det delade elementet. Överdriven användning (mer än 3 delade element per övergång) minskar dock effekten och kan orsaka FPS-fall under 40 på medelklassiga enheter.
I Android implementeras Shared Element Transition via FragmentTransaction.addSharedElement(view, transitionName) eller ActivityOptions.makeSceneTransitionAnimation. Varje View tilldelas en unik android:transitionName — samma på båda skärmarna. Systemet skapar automatiskt en animering för förflyttning (ChangeBounds), storleksändring (ChangeTransform) och bildskalning (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 via 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 via 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()
// Anpassa övergång: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 400L
interpolator = FastOutSlowInInterpolator()
}
Viktiga Android-detaljer: för korrekt animering av ImageView med olika scaleType (centerCrop → fitCenter) använd ChangeImageTransform — den animerar automatiskt förändringen av visningssätt. För View med rundade hörn (CardView) lägg till ChangeBounds, som animerar hörnradien via outline. För textanimering (teckenstorleksändring) använd ChangeTextTransform. Om det delade elementet försvinner eller visas på en av skärmarna — tillämpar systemet automatiskt Fade.
I iOS finns ingen inbyggd motsvarighet till Androids sharedElementEnterTransition — delade animeringar implementeras manuellt via UIViewControllerAnimatedTransitioning med UIViewPropertyAnimator eller CASpringAnimation. Huvudteknik: i animateTransition(using:) ta View från källskärmen, skapa en snapshot, lägg till i containerView, dölj originalet och animera snapshot till målpositionen. I slutet visa mål-View och ta bort snapshot.
import UIKit
"> Anpassad Shared Element-animator
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 av delat element
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 — bibliotek för att förenkla shared transitions
// heroID matchar på båda skärmarna, analogt med transitionName i Android
import Hero
imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"
UIViewPropertyAnimator — den föredragna metoden för delade animeringar på iOS 10+. Till skillnad från UIView.animate stöder PropertyAnimator interaktiv kontroll (fractionComplete), vilket möjliggör integration av shared transition med swipe-back-gesten. För Hero-biblioteket (tredje part) ställs shared transition in via hero.id — detta är motsvarigheten till transitionName från Android, som automatiskt animerar position, storlek, cornerRadius, zPosition och backgroundColor mellan skärmar.
I Jetpack Compose implementeras Shared Element Transition via modifieraren graphicsLayer med SharedTransitionScope-animering (Compose 1.7+). Varje delat element markeras via sharedElement() med en unik nyckel. Systemet animerar automatiskt förändringen av position, storlek, form, färg och beskärningsmask under navigering mellan composable-funktioner. Enligt Android Developers Blog (2026) stöder Compose Shared Element Transition AnimatedContent, NavHost och anpassade containrar.
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 — container för delade element
SharedTransitionLayout {
val sharedTransitionState = remember { SharedTransitionState() }
// Skärm 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)
}
}
}
// Skärm B: detaljer (visas när element väljs)
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 — nytt API tillgängligt i experimentellt läge. Ersätter manuell hantering av delade animeringar i NavHost. Viktiga funktioner: boundsTransform för anpassad animeringskurva, clipToBounds för rundade hörn, zIndex för lagerordning. För animering av bilder med olika contentScale använd Modifier.sharedElement med SharedContentKey-omslag. För stöd av äldre Compose-versioner (< 1.7) använd SharedTransitionScope-biblioteket från Google.
Shared Element Transition kan orsaka prestandaproblem om plattformsbegränsningar inte beaktas. I Android är huvudproblemet ChangeImageTransform med stora bilder (över 2048 px). I iOS — snapshot med tunga lager (CAGradientLayer, CAShapeLayer med många noder). Lösning: använd komprimerade bilder för delade animeringar och ta snapshot med afterScreenUpdates: false.
| Problem | Orsak | Lösning |
|---|---|---|
| Hackig animering | Olika layout före/efter övergång / tung snapshot | Använd Placeholder: visa liten förhandsvisning under laddning |
| Delat element animeras inte | transitionName matchar inte på båda skärmarna | Kontrollera exakt matchning av transitionName i XML/kod |
| FPS < 40 på Android | Delat element innehåller komplex View-hierarki | Dela endast ImageView, inte hela CardView |
| iOS: snapshot-artefakter | renderInContext på lager med masker/animeringar | Använd drawHierarchy(in:afterScreenUpdates: true) |
| Compose: element visas inte | SharedTransitionLayout omsluter inte båda skärmarna | SharedTransitionLayout måste vara förälder för båda |
// Optimering: Placeholder för bild i shared transition
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> Laddar preview före navigering
Glide.with(context)
.load(imageUrl)
.override(300, 300) // Storleksbegränsning för shared transition
.placeholder(R.drawable.placeholder)
.into(imageView)
> Fördröjd inläsning av full-resolution efter övergången
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS: optimering av snapshot med drawHierarchy
// snapshotView(afterScreenUpdates: false) — snabbare, ignorerar väntande uppdateringar
// För komplexa View: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
Bästa praxis: lägg inte till mer än 3 delade element per övergång — varje extra element ökar renderingsbelastningen. För listor (RecyclerView/UICollectionView) gör det delade elementet på det klickade objektet, inte på hela listan. För kort med rundade hörn använd clipToOutline i Android eller cornerRadius i snapshot i iOS. Om det delade elementet på den andra skärmen finns i en ScrollView, vänta på att layouten slutförs (view.doOnPreDraw) innan animeringen startar.
Vanliga frågor
Rekommenderat är inte mer än 3 delade element per övergång. Varje extra element ökar renderingsbelastningen och kan orsaka FPS-fall. Optimalt: 1 bild + 1 text (titel). För Android läggs varje element till via addSharedElement(view, transitionName). För iOS läggs varje snapshot till i containerView separat.
Se till att transitionName ställs in dynamiskt i onBindViewHolder: holder.imageView.transitionName = «image_${position}». För FragmentTransaction.addSharedElement skicka den specifika View från holder. För ActivityOptions använd Pair.create(holder.imageView, holder.imageView.transitionName). Skjut upp shared transition tills layouten är slutförd via postponeEnterTransition() och startPostponedEnterTransition().
I Android: lägg till ChangeBackground i TransitionSet eller använd en anpassad Transition som åsidosätter captureStartValues och captureEndValues. I iOS: animera snapshot:ens backgroundColor eller UIViewPropertyAnimator med färgändring. I Jetpack Compose: Modifier.sharedElement animerar automatiskt background via Color transition. För kort, animera även elevation — från 2dp på listan till 8dp på detaljskärmen.
Ja, med Navigation Component stöds Shared Element via NavOptions: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). Använd FragmentNavigatorExtras för att skicka delade element: FragmentNavigatorExtras(view to transitionName). För Navigation Compose använd Modifier.sharedElement från SharedTransitionScope (Compose 1.7+).
I Android aktivera Transition Debugging: adb shell setprop debug.transition 1 — i logcat visas meddelanden om infångning och animering av delade element. Aktivera «Show layout bounds» i Developer Options för att kontrollera gränser före/efter. I iOS använd Debug View Hierarchy för att kontrollera snapshot i containerView. För FPS-övervakning använd Profile GPU Rendering (Android) eller Core Animation instrument (iOS).
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också