Shared Element Transition — انیمیشنی است که در آن یک عنصر مشترک (تصویر، متن، کارت) به آرامی بین دو صفحه در طول ناوبری جابهجا و تغییر شکل مییابد. کاربر به جای یک گذر ناگهانی، حرکتی پیوسته میبیند: عنصر از صفحه A به صفحه B «جریان مییابد» و زمینه و ارتباط بصری را حفظ میکند. طبق Material Design Guidelines (2026)، Shared Element Transition عملکرد درک شده را 35% افزایش و درک ناوبری را 28% بهبود میبخشد. درباره سایر انواع انیمیشن در راهنمای کلی انیمیشن بخوانید.
نکات اصلی
Shared Element Transition — انیمیشن تغییر صفحهای است که در آن یک یا چند عنصر بصری از صفحه قبلی به صفحه بعدی «نقل مکان میکنند». در لحظه ناوبری، سیستم از عنصر در صفحه مبدأ snapshot میگیرد، تغییر شکل آن (موقعیت، اندازه، شکل، رنگ) را به حالت مقصد در صفحه جدید متحرک میکند. در نتیجه کاربر دو صفحه را بهعنوان بخشهایی از یک فضای واحد درک میکند، نه بهعنوان صفحات جداگانه.
سناریوهای اصلی استفاده: کارت محصول → صفحه جزئیات (تصویر بزرگ شده و به بالا میرود)، گالری تصاویر → نمایش (تصویر به آرامی در کل صفحه باز میشود)، لیست مخاطبین → پروفایل (آواتار و نام جابهجا میشوند). برای عملکرد صحیح، هر دو صفحه باید View با transitionName یکسان (Android) یا شناسه snapshot (iOS) داشته باشند.
طبق Google I/O 2025، Shared Element Transition یکی از سه الگوی UX مؤثر برای برنامههای موبایل است: کاربرانی که انیمیشن عنصر مشترک را دیدهاند، 22% کمتر بلافاصله پس از گذر بازمیگردند. با این حال، استفاده بیش از حد (بیش از 3 عنصر مشترک در هر گذر) اثر را کاهش میدهد و میتواند باعث افت FPS به زیر 40 در دستگاههای میانرده شود.
در Android، Shared Element Transition از طریق FragmentTransaction.addSharedElement(view, transitionName) یا ActivityOptions.makeSceneTransitionAnimation پیادهسازی میشود. به هر View یک android:transitionName یکتا اختصاص داده میشود — یکسان در هر دو صفحه. سیستم به طور خودکار انیمیشن جابهجایی (ChangeBounds)، تغییر اندازه (ChangeTransform) و مقیاسبندی تصویر (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 از طریق 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 را اعمال میکند.
در iOS معادل داخلی sharedElementEnterTransition اندروید وجود ندارد — انیمیشنهای اشتراکی به صورت دستی از طریق UIViewControllerAnimatedTransitioning با استفاده از UIViewPropertyAnimator یا CASpringAnimation پیادهسازی میشوند. تکنیک اصلی: در animateTransition(using:) View صفحه مبدأ را بگیرید، از آن snapshot بگیرید، به containerView اضافه کنید، اصل را پنهان کنید و snapshot را به موقعیت مقصد متحرک کنید. در پایان View مقصد را نشان داده و snapshot را حذف کنید.
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 — روش ترجیحی برای انیمیشنهای اشتراکی در iOS 10+. برخلاف UIView.animate، PropertyAnimator از کنترل تعاملی (fractionComplete) پشتیبانی میکند که امکان ادغام shared transition با ژست swipe-back را فراهم میکند. برای کتابخانه Hero (شخص ثالث)، shared transition از طریق hero.id تنظیم میشود — معادل transitionName در Android است که به طور خودکار موقعیت، اندازه، cornerRadius، zPosition و backgroundColor را بین صفحهها متحرک میکند.
در Jetpack Compose، Shared Element Transition از طریق اصلاحکننده graphicsLayer با انیمیشن SharedTransitionScope (Compose 1.7+) پیادهسازی میشود. هر عنصر اشتراکی با sharedElement() و یک کلید یکتا مشخص میشود. سیستم به طور خودکار تغییر موقعیت، اندازه، شکل، رنگ و ماسک برش را در طول ناوبری بین توابع composable متحرک میکند. طبق Android Developers Blog (2026)، Compose Shared Element Transition از AnimatedContent، NavHost و کانتینرهای سفارشی پشتیبانی میکند.
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 با wrapper SharedContentKey استفاده کنید. برای پشتیبانی از نسخههای قدیمیتر Compose (< 1.7) از کتابخانه SharedTransitionScope گوگل استفاده کنید.
Shared Element Transition در صورت عدم توجه به محدودیتهای پلتفرم میتواند مشکلات عملکردی ایجاد کند. در Android مشکل اصلی ChangeImageTransform با تصاویر بزرگ (بالای 2048 px) است. در iOS — snapshot با لایههای سنگین (CAGradientLayer، CAShapeLayer با تعداد زیادی گره). راهحل: از تصاویر فشرده برای انیمیشن اشتراکی استفاده کنید و snapshot را با afterScreenUpdates: false بگیرید.
| مشکل | علت | راهحل |
|---|---|---|
| انیمیشن ناپیوسته | layout متفاوت قبل و بعد از گذر / snapshot سنگین | از Placeholder استفاده کنید: پیشنمایش کوچک در هنگام بارگذاری نمایش دهید |
| عنصر اشتراکی متحرک نمیشود | transitionName در هر دو صفحه مطابقت ندارد | تطبیق دقیق transitionName را در XML/کد بررسی کنید |
| FPS < 40 در Android | عنصر اشتراکی شامل سلسلهمراتب پیچیده View است | فقط ImageView را اشتراکی کنید، نه کل CardView را |
| iOS: مصنوعات snapshot | renderInContext روی لایه با ماسکها/انیمیشنها | از drawHierarchy(in:afterScreenUpdates: true) استفاده کنید |
| Compose: عنصر ظاهر نمیشود | SharedTransitionLayout هر دو صفحه را دربر نمیگیرد | SharedTransitionLayout باید والد هر دو باشد |
// بهینهسازی: 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) — سریعتر، بهروزرسانیهای معلق را نادیده میگیرد
// برای Viewهای پیچیده: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
بهترین روشها: بیش از 3 عنصر اشتراکی در یک گذر اضافه نکنید — هر عنصر اضافی بار رندر را افزایش میدهد. برای لیستها (RecyclerView/UICollectionView) عنصر اشتراکی را روی آیتم کلیک شده انجام دهید، نه کل لیست. برای کارتهای با گوشههای گرد از clipToOutline در Android یا cornerRadius در snapshot در iOS استفاده کنید. اگر عنصر اشتراکی در صفحه دوم در ScrollView است، قبل از شروع انیمیشن منتظر اتمام layout بمانید (view.doOnPreDraw).
سوالات متداول
توصیه میشود بیش از 3 عنصر اشتراکی در یک گذر نباشد. هر عنصر اضافی بار رندر را افزایش داده و میتواند باعث افت FPS شود. بهینه: 1 تصویر + 1 متن (عنوان). برای Android هر عنصر از طریق addSharedElement(view, transitionName) اضافه میشود. برای iOS هر snapshot به طور جداگانه به containerView اضافه میشود.
مطمئن شوید transitionName به صورت پویا در onBindViewHolder تنظیم شده است: holder.imageView.transitionName = «image_${position}». برای FragmentTransaction.addSharedElement View خاص از holder را ارسال کنید. برای ActivityOptions از Pair.create(holder.imageView, holder.imageView.transitionName) استفاده کنید. Shared transition را تا اتمام layout از طریق postponeEnterTransition() و startPostponedEnterTransition() به تأخیر بیندازید.
در Android: ChangeBackground را به TransitionSet اضافه کنید یا از Transition سفارشی استفاده کنید که captureStartValues و captureEndValues را بازنویسی میکند. در iOS: backgroundColor snapshot یا UIViewPropertyAnimator را با تغییر رنگ متحرک کنید. در Jetpack Compose: Modifier.sharedElement به طور خودکار background را از طریق Color transition متحرک میکند. برای کارتها همچنین elevation را متحرک کنید — از 2dp در لیست تا 8dp در صفحه جزئیات.
بله، با 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+) استفاده کنید.
در Android Transition Debugging را فعال کنید: adb shell setprop debug.transition 1 — در logcat پیامهایی درباره ضبط و انیمیشن عناصر اشتراکی ظاهر میشود. «Show layout bounds» را در Developer Options برای بررسی مرزهای قبل/بعد فعال کنید. در iOS از Debug View Hierarchy برای بررسی snapshot در containerView استفاده کنید. برای پایش FPS از Profile GPU Rendering (Android) یا Core Animation instrument (iOS) استفاده کنید.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید