Shared Element Transition هي رسوم متحركة يتحرك ويتحول خلالها عنصر مشترك (صورة، نص، بطاقة) بسلاسة بين شاشتين أثناء التنقل. بدلاً من الانتقال المفاجئ، يرى المستخدم حركة مستمرة: العنصر من الشاشة A ينتقل إلى الشاشة B، محافظاً على السياق والاتصال البصري. وفقاً لـ Material Design Guidelines (2026)، يزيد Shared Element Transition من الأداء المدرك بنسبة 35% ويحسن فهم التنقل بنسبة 28%. تعرف على المزيد حول أنواع الرسوم المتحركة الأخرى في الدليل العام للرسوم المتحركة.
أهم النقاط
Shared Element Transition هي رسوم متحركة لانتقال الشاشة حيث يتحرك عنصر بصري واحد أو أكثر من الشاشة السابقة إلى التالية. في لحظة التنقل، يأخذ النظام لقطة للعنصر على الشاشة المصدر، ويحرك تحوله (الموضع، الحجم، الشكل، اللون) إلى الحالة المستهدفة على الشاشة الجديدة. ونتيجة لذلك، يدرك المستخدم الشاشتين كجزء من مساحة واحدة بدلاً من صفحات منفصلة.
حالات الاستخدام الرئيسية: بطاقة المنتج ← شاشة التفاصيل (تتكبر الصورة وتتحرك للأعلى)، معرض الصور ← العارض (تتمدد الصورة بسلاسة إلى ملء الشاشة)، قائمة جهات الاتصال ← الملف الشخصي (يتحرك الصورة الرمزية والاسم). لكي يعمل بشكل صحيح، يجب أن تحتوي كلتا الشاشتين على Views بنفس transitionName (Android) أو معرف اللقطة (iOS).
وفقاً لـ Google I/O 2025، يعد Shared Element Transition من بين أكثر ثلاثة أنماط UX فعالية لتطبيقات الجوال: حيث يقل احتمال عودة المستخدمين فوراً بعد الانتقال بنسبة 22% إذا شاهدوا رسوم العنصر المشترك. ومع ذلك، فإن الاستخدام المفرط (أكثر من 3 عناصر مشتركة لكل انتقال) يقلل من التأثير وقد يتسبب في انخفاض FPS إلى أقل من 40 على الأجهزة متوسطة المدى.
في Android، يتم تنفيذ Shared Element Transition عبر FragmentTransaction.addSharedElement(view, transitionName) أو ActivityOptions.makeSceneTransitionAnimation. يتم تعيين android:transitionName فريد لكل View — نفس القيمة على كلتا الشاشتين. يقوم النظام تلقائياً بإنشاء رسوم متحركة للحركة (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 — فهو يحرك تلقائياً تغيير وضع العرض. للـ Views ذات الزوايا الدائرية (CardView)، أضف ChangeBounds الذي يحرك نصف قطر الزوايا عبر outline. لرسوم النص المتحركة (تغيير حجم الخط)، استخدم ChangeTextTransform. إذا اختفى عنصر مشترك أو ظهر على إحدى الشاشتين، يطبق النظام تلقائياً Fade.
لا يحتوي iOS على مكافئ مدمج لـ sharedElementEnterTransition في Android — يتم تنفيذ الرسوم المتحركة المشتركة يدوياً عبر UIViewControllerAnimatedTransitioning باستخدام UIViewPropertyAnimator أو CASpringAnimation. التقنية الرئيسية: في animateTransition(using:)، احصل على View من الشاشة المصدر، التقط لقطة، أضفها إلى containerView، أخفِ الأصل، وحرك اللقطة إلى الموضع المستهدف. في النهاية، أظهر View الهدف وأزل اللقطة.
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
}
// لقطة العنصر المشترك
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 — مكتبة لتبسيط الانتقالات المشتركة
// 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)، مما يسمح بدمج الانتقال المشترك مع إيماءة السحب للخلف. لمكتبة Hero (جهة خارجية)، يتم تعيين الانتقال المشترك عبر 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 — واجهة برمجة تطبيقات جديدة متاحة في الوضع التجريبي. تحل محل الإدارة اليدوية للرسوم المتحركة المشتركة في NavHost. الميزات الرئيسية: boundsTransform لمنحنى رسوم متحركة مخصص، clipToBounds للزوايا الدائرية، zIndex لترتيب التراص. لتحريك الصور بـ contentScale مختلف، استخدم Modifier.sharedElement مع غلاف SharedContentKey. للإصدارات الأقدم من Compose (< 1.7)، استخدم مكتبة SharedTransitionScope من Google.
Shared Element Transition قد يسبب مشاكل في الأداء إذا لم تؤخذ قيود المنصة في الاعتبار. في Android، المشكلة الرئيسية هي ChangeImageTransform مع الصور الكبيرة (فوق 2048 بكسل). في iOS — اللقطات ذات الطبقات الثقيلة (CAGradientLayer، CAShapeLayer مع العديد من العقد). الحل: استخدم صوراً مضغوطة للرسوم المتحركة المشتركة والتقط اللقطة مع afterScreenUpdates: false.
| المشكلة | السبب | الحل |
|---|---|---|
| الرسوم المتحركة متقطعة | تخطيط مختلف قبل/بعد الانتقال / لقطة ثقيلة | استخدم Placeholder: أظهر معاينة صغيرة أثناء التحميل |
| العنصر المشترك لا يتحرك | transitionName غير متطابق على كلتا الشاشتين | تحقق من التطابق التام لـ transitionName في XML/الكود |
| FPS < 40 على Android | العنصر المشترك يحتوي على تسلسل هرمي معقد للـ View | شارك فقط ImageView، وليس CardView بالكامل |
| iOS: تشوهات اللقطة | renderInContext على طبقة بأقنعة/رسوم متحركة | استخدم drawHierarchy(in:afterScreenUpdates: true) |
| Compose: العنصر لا يظهر | SharedTransitionLayout لا يحيط بكلتا الشاشتين | يجب أن يكون SharedTransitionLayout هو الأصل لكلتيهما |
// تحسين: Placeholder للصورة في الانتقال المشترك
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> تحميل preview قبل التنقل
Glide.with(context)
.load(imageUrl)
.override(300, 300) // حد الحجم للانتقال المشترك
.placeholder(R.drawable.placeholder)
.into(imageView)
> تحميل مؤجل بدقة كاملة بعد الانتقال
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS: تحسين اللقطة باستخدام drawHierarchy
// snapshotView(afterScreenUpdates: false) — أسرع، يتجاهل التحديثات المعلقة
// لـ Views المعقدة: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
أفضل الممارسات: لا تضف أكثر من 3 عناصر مشتركة لكل انتقال — كل عنصر إضافي يزيد من حمل العرض. للقوائم (RecyclerView/UICollectionView)، اجعل العنصر المشترك هو العنصر الذي تم النقر عليه، وليس القائمة بأكملها. للبطاقات ذات الزوايا الدائرية، استخدم clipToOutline في Android أو cornerRadius في اللقطة في iOS. إذا كان العنصر المشترك على الشاشة الثانية داخل ScrollView، انتظر حتى اكتمال التخطيط (view.doOnPreDraw) قبل بدء الرسوم المتحركة.
الأسئلة الشائعة
يوصى باستخدام لا يزيد عن 3 عناصر مشتركة لكل انتقال. كل عنصر إضافي يزيد من حمل العرض وقد يتسبب في انخفاض FPS. الأمثل: صورة واحدة + نص واحد (عنوان). في Android، يضاف كل عنصر عبر addSharedElement(view, transitionName). في iOS، تضاف كل لقطة إلى containerView بشكل منفصل.
تأكد من تعيين transitionName ديناميكياً في onBindViewHolder: holder.imageView.transitionName = "image_${position}". لـ FragmentTransaction.addSharedElement، مرر View المحددة من holder. لـ ActivityOptions، استخدم Pair.create(holder.imageView, holder.imageView.transitionName). أخر الانتقال المشترك حتى اكتمال التخطيط عبر postponeEnterTransition() و startPostponedEnterTransition().
في Android: أضف ChangeBackground إلى TransitionSet أو استخدم Transition مخصصاً يتجاوز captureStartValues و captureEndValues. في iOS: حرك backgroundColor للقطة أو استخدم UIViewPropertyAnimator مع تغيير اللون. في Jetpack Compose: Modifier.sharedElement يحرك الخلفية تلقائياً عبر انتقال Color. للبطاقات، حرك أيضاً الارتفاع — من 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 للتحقق من اللقطة في containerView. لـ FPS، استخدم Profile GPU Rendering (Android) أو أداة Core Animation (iOS).
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا