Shared Element Transition — animasi di mana elemen bersama (gambar, teks, kartu) bergerak dan bertransformasi secara halus antara dua layar selama navigasi. Alih-alih transisi mendadak, pengguna melihat gerakan berkelanjutan: elemen dari layar A «mengalir» ke layar B, mempertahankan konteks dan hubungan visual. Menurut Material Design Guidelines (2026), Shared Element Transition meningkatkan performa yang dirasakan sebesar 35% dan meningkatkan pemahaman navigasi sebesar 28%. Baca lebih lanjut tentang jenis animasi lainnya di panduan animasi umum.
Poin Utama
Shared Element Transition adalah animasi pergantian layar di mana satu atau lebih elemen visual «pindah» dari layar sebelumnya ke layar berikutnya. Pada saat navigasi, sistem mengambil snapshot elemen di layar sumber, menganimasikan transformasinya (posisi, ukuran, bentuk, warna) ke keadaan target di layar baru. Hasilnya, pengguna merasakan dua layar sebagai bagian dari satu ruang, bukan sebagai halaman terpisah.
Skenario penggunaan utama: kartu produk → layar detail (gambar membesar dan pindah ke atas), galeri gambar → tampilan (gambar terbuka secara halus ke layar penuh), daftar kontak → profil (avatar dan nama berpindah). Agar berfungsi dengan benar, kedua layar harus memiliki View dengan transitionName yang sama (Android) atau pengidentifikasi snapshot (iOS).
Menurut Google I/O 2025, Shared Element Transition adalah salah satu dari tiga pola UX paling efektif untuk aplikasi seluler: pengguna 22% lebih jarang kembali segera setelah transisi jika mereka melihat animasi elemen bersama. Namun, penggunaan berlebihan (lebih dari 3 elemen bersama per transisi) mengurangi efek dan dapat menyebabkan penurunan FPS di bawah 40 pada perangkat kelas menengah.
Di Android, Shared Element Transition diimplementasikan melalui FragmentTransaction.addSharedElement(view, transitionName) atau ActivityOptions.makeSceneTransitionAnimation. Setiap View diberi android:transitionName unik — sama di kedua layar. Sistem secara otomatis membuat animasi perpindahan (ChangeBounds), perubahan ukuran (ChangeTransform) dan penskalaan gambar (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 melalui 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 melalui 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()
// Kustomisasi transisi: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 400L
interpolator = FastOutSlowInInterpolator()
}
Detail Penting Android: untuk animasi ImageView yang benar dengan scaleType berbeda (centerCrop → fitCenter) gunakan ChangeImageTransform — ia secara otomatis menganimasikan perubahan cara tampilan. Untuk View dengan sudut membulat (CardView) tambahkan ChangeBounds, yang menganimasikan radius sudut melalui outline. Untuk animasi teks (perubahan ukuran font) gunakan ChangeTextTransform. Jika elemen bersama menghilang atau muncul di salah satu layar — sistem secara otomatis menerapkan Fade.
Di iOS tidak ada padanan bawaan untuk sharedElementEnterTransition Android — animasi bersama diimplementasikan secara manual melalui UIViewControllerAnimatedTransitioning menggunakan UIViewPropertyAnimator atau CASpringAnimation. Teknik utama: di animateTransition(using:) ambil View dari layar sumber, buat snapshot-nya, tambahkan ke containerView, sembunyikan aslinya dan animasikan snapshot ke posisi target. Setelah selesai, tampilkan View target dan hapus snapshot.
import UIKit
"> Animator Shared Element kustom
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 elemen bersama
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 — pustaka untuk menyederhanakan shared transitions
// heroID cocok di kedua layar, mirip dengan transitionName di Android
import Hero
imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"
UIViewPropertyAnimator — cara yang lebih disukai untuk animasi bersama di iOS 10+. Tidak seperti UIView.animate, PropertyAnimator mendukung kontrol interaktif (fractionComplete), memungkinkan integrasi shared transition dengan gerakan swipe-back. Untuk pustaka Hero (pihak ketiga), shared transition diatur melalui hero.id — ini adalah analog dari transitionName di Android, yang secara otomatis menganimasikan posisi, ukuran, cornerRadius, zPosition dan backgroundColor antar layar.
Di Jetpack Compose, Shared Element Transition diimplementasikan melalui pengubah graphicsLayer dengan animasi SharedTransitionScope (Compose 1.7+). Setiap elemen bersama ditandai melalui sharedElement() dengan kunci unik. Sistem secara otomatis menganimasikan perubahan posisi, ukuran, bentuk, warna dan masker pemotongan selama navigasi antara fungsi composable. Menurut Android Developers Blog (2026), Compose Shared Element Transition mendukung AnimatedContent, NavHost dan wadah kustom.
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 — wadah untuk elemen bersama
SharedTransitionLayout {
val sharedTransitionState = remember { SharedTransitionState() }
// Layar A: daftar
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)
}
}
}
// Layar B: detail (ditampilkan saat memilih elemen)
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 baru yang tersedia dalam mode eksperimental. Menggantikan manajemen manual animasi bersama di NavHost. Kemampuan utama: boundsTransform untuk kurva animasi kustom, clipToBounds untuk sudut membulat, zIndex untuk urutan tumpukan. Untuk animasi gambar dengan contentScale berbeda, gunakan Modifier.sharedElement dengan pembungkus SharedContentKey. Untuk dukungan versi Compose yang lebih lama (< 1.7) gunakan pustaka SharedTransitionScope dari Google.
Shared Element Transition dapat menyebabkan masalah kinerja jika batasan platform tidak dipertimbangkan. Di Android masalah utama adalah ChangeImageTransform dengan gambar besar (di atas 2048 px). Di iOS — snapshot dengan lapisan berat (CAGradientLayer, CAShapeLayer dengan banyak simpul). Solusi: gunakan gambar terkompresi untuk animasi bersama dan buat snapshot dengan afterScreenUpdates: false.
| Masalah | Penyebab | Solusi |
|---|---|---|
| Animasi tersendat | Layout berbeda sebelum/sesudah transisi / snapshot berat | Gunakan Placeholder: tampilkan pratinjau kecil saat memuat |
| Elemen bersama tidak dianimasikan | transitionName tidak cocok di kedua layar | Periksa kecocokan tepat transitionName di XML/kode |
| FPS < 40 di Android | Elemen bersama berisi hierarki View yang kompleks | Buat shared hanya ImageView, bukan seluruh CardView |
| iOS: artefak snapshot | renderInContext pada lapisan dengan mask/animasi | Gunakan drawHierarchy(in:afterScreenUpdates: true) |
| Compose: elemen tidak muncul | SharedTransitionLayout tidak membungkus kedua layar | SharedTransitionLayout harus menjadi induk untuk keduanya |
// Optimasi: Placeholder untuk gambar di shared transition
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> Memuat preview sebelum navigasi
Glide.with(context)
.load(imageUrl)
.override(300, 300) // Batasan ukuran untuk shared transition
.placeholder(R.drawable.placeholder)
.into(imageView)
> Pemuatan full-resolution yang ditunda setelah transisi
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS: optimasi snapshot dengan drawHierarchy
// snapshotView(afterScreenUpdates: false) — lebih cepat, mengabaikan pembaruan yang tertunda
// Untuk View yang kompleks: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
Praktik Terbaik: jangan menambahkan lebih dari 3 elemen bersama per transisi — setiap elemen tambahan meningkatkan beban rendering. Untuk daftar (RecyclerView/UICollectionView) buat elemen bersama pada item yang diklik, bukan pada seluruh daftar. Untuk kartu dengan sudut membulat gunakan clipToOutline di Android atau cornerRadius di snapshot di iOS. Jika elemen bersama di layar kedua berada di ScrollView, tunggu layout selesai (view.doOnPreDraw) sebelum memulai animasi.
Pertanyaan yang Sering Diajukan
Disarankan tidak lebih dari 3 elemen bersama per transisi. Setiap elemen tambahan meningkatkan beban rendering dan dapat menyebabkan penurunan FPS. Optimal: 1 gambar + 1 teks (judul). Untuk Android setiap elemen ditambahkan melalui addSharedElement(view, transitionName). Untuk iOS setiap snapshot ditambahkan ke containerView secara terpisah.
Pastikan transitionName diatur secara dinamis di onBindViewHolder: holder.imageView.transitionName = «image_${position}». Untuk FragmentTransaction.addSharedElement berikan View spesifik dari holder. Untuk ActivityOptions gunakan Pair.create(holder.imageView, holder.imageView.transitionName). Tunda shared transition hingga layout selesai melalui postponeEnterTransition() dan startPostponedEnterTransition().
Di Android: tambahkan ChangeBackground ke TransitionSet atau gunakan Transition kustom yang menimpa captureStartValues dan captureEndValues. Di iOS: animasikan backgroundColor snapshot atau UIViewPropertyAnimator dengan perubahan warna. Di Jetpack Compose: Modifier.sharedElement secara otomatis menganimasikan background melalui Color transition. Untuk kartu, animasikan juga elevation — dari 2dp di daftar hingga 8dp di layar detail.
Ya, dengan Navigation Component Shared Element didukung melalui NavOptions: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). Gunakan FragmentNavigatorExtras untuk meneruskan elemen bersama: FragmentNavigatorExtras(view to transitionName). Untuk Navigation Compose gunakan Modifier.sharedElement dari SharedTransitionScope (Compose 1.7+).
Di Android aktifkan Transition Debugging: adb shell setprop debug.transition 1 — di logcat akan muncul pesan tentang penangkapan dan animasi elemen bersama. Aktifkan «Show layout bounds» di Developer Options untuk memeriksa batas sebelum/sesudah. Di iOS gunakan Debug View Hierarchy untuk memeriksa snapshot di containerView. Untuk pemantauan FPS gunakan Profile GPU Rendering (Android) atau Core Animation instrument (iOS).
Kesimpulan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga