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 اندروید برای تنظیم ورود عنصر مشترک.
  • UIViewPropertyAnimator — پایه انیمیشن‌های اشتراکی سفارشی بین UIViewController در iOS.
  • عنصر مشترک از طریق transitionName در Android و tag یا snapshot در iOS تنظیم می‌شود.
  • انیمیشن با ImageView، TextView، CardView، Button و هر View با شناسه یکتا کار می‌کند.

Shared Element Transition چیست؟

Shared Element Transition — انیمیشن تغییر صفحه‌ای است که در آن یک یا چند عنصر بصری از صفحه قبلی به صفحه بعدی «نقل مکان می‌کنند». در لحظه ناوبری، سیستم از عنصر در صفحه مبدأ snapshot می‌گیرد، تغییر شکل آن (موقعیت، اندازه، شکل، رنگ) را به حالت مقصد در صفحه جدید متحرک می‌کند. در نتیجه کاربر دو صفحه را به‌عنوان بخش‌هایی از یک فضای واحد درک می‌کند، نه به‌عنوان صفحات جداگانه.

سناریوهای اصلی استفاده: کارت محصول → صفحه جزئیات (تصویر بزرگ شده و به بالا می‌رود)، گالری تصاویر → نمایش (تصویر به آرامی در کل صفحه باز می‌شود)، لیست مخاطبین → پروفایل (آواتار و نام جابه‌جا می‌شوند). برای عملکرد صحیح، هر دو صفحه باید View با transitionName یکسان (Android) یا شناسه snapshot (iOS) داشته باشند.

طبق Google I/O 2025، Shared Element Transition یکی از سه الگوی UX مؤثر برای برنامه‌های موبایل است: کاربرانی که انیمیشن عنصر مشترک را دیده‌اند، 22% کمتر بلافاصله پس از گذر بازمی‌گردند. با این حال، استفاده بیش از حد (بیش از 3 عنصر مشترک در هر گذر) اثر را کاهش می‌دهد و می‌تواند باعث افت 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 استفاده کنید. اگر عنصر مشترک در یکی از صفحه‌ها ناپدید یا ظاهر می‌شود — سیستم به طور خودکار Fade را اعمال می‌کند.

Shared Element Transition در iOS (Swift)

در iOS معادل داخلی sharedElementEnterTransition اندروید وجود ندارد — انیمیشن‌های اشتراکی به صورت دستی از طریق 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 — روش ترجیحی برای انیمیشن‌های اشتراکی در 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+) پیاده‌سازی می‌شود. هر عنصر اشتراکی با sharedElement() و یک کلید یکتا مشخص می‌شود. سیستم به طور خودکار تغییر موقعیت، اندازه، شکل، رنگ و ماسک برش را در طول ناوبری بین توابع 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 — کانتینر برای عناصر اشتراکی
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: مصنوعات 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) — سریع‌تر، به‌روزرسانی‌های معلق را نادیده می‌گیرد
// برای 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 اضافه می‌شود.

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 استفاده کنید: 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 پیام‌هایی درباره ضبط و انیمیشن عناصر اشتراکی ظاهر می‌شود. «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 عنصر اشتراکی در هر گذر برای جلوگیری از افت FPS.
  • برای ImageView با scaleType متفاوت از ChangeImageTransform (Android) یا snapshot (iOS) استفاده کنید.
  • کتابخانه Hero (iOS) — راه‌حل شخص ثالث با hero.id، مشابه transitionName در Android.

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید