تُعد عمليات الانتقال بين العناصر المشترَكة طريقة سلسة للانتقال بين عناصر قابلة للإنشاء تتضمّن محتوًى متسقًا بينها. ويتم استخدامها غالبًا للتنقل، ما يتيح لك ربط شاشات مختلفة بشكل مرئي أثناء تنقّل المستخدم بينها.
على سبيل المثال، في الفيديو التالي، يمكنك الاطّلاع على صورة الوجبة الخفيفة وعنوانها، وهما مشارَكان من صفحة البطاقة إلى صفحة التفاصيل.
في Compose، هناك بعض واجهات برمجة التطبيقات ذات المستوى العالي التي تساعدك في إنشاء عناصر مشتركة:
-
SharedTransitionLayout
: هو التصميم الخارجي المطلوب لتنفيذ عمليات الانتقال بين العناصر المشتركة. يوفّرSharedTransitionScope
. يجب أن تكون العناصر القابلة للإنشاء داخلSharedTransitionScope
لاستخدام معدِّلات العناصر المشترَكة. -
Modifier.sharedElement()
: المعدِّل الذي يشير إلىSharedTransitionScope
العنصر القابل للإنشاء الذي يجب مطابقته مع عنصر آخر قابل للإنشاء. Modifier.sharedBounds()
: المعدِّل الذي يشير إلىSharedTransitionScope
بأنّه يجب استخدام حدود هذا العنصر القابل للإنشاء كحدود الحاوية التي سيحدث فيها الانتقال. على عكسsharedElement()
، تم تصميمsharedBounds()
للمحتوى الذي يختلف بصريًا.
من المفاهيم المهمة عند إنشاء عناصر مشترَكة في Compose طريقة عملها مع التراكبات والقص. اطّلِع على قسم القصاصات والتراكبات لمعرفة المزيد حول هذا الموضوع المهم.
الاستخدام الأساسي
سيتم إنشاء الانتقال التالي في هذا القسم، بدءًا من عنصر "القائمة" الأصغر حجمًا إلى عنصر التفاصيل الأكبر حجمًا:

أفضل طريقة لاستخدام Modifier.sharedElement()
هي بالاقتران مع AnimatedContent
أو AnimatedVisibility
أو NavHost
، لأنّ ذلك يدير عملية الانتقال بين العناصر القابلة للإنشاء تلقائيًا.
نقطة البداية هي AnimatedContent
أساسي حالي يتضمّن MainContent
وDetailsContent
قابل للإنشاء قبل إضافة العناصر المشترَكة:

AnimatedContent
بدون أي انتقالات بين العناصر المشتركةلتحريك العناصر المشترَكة بين التصميمَين، ضَع العنصر القابل للإنشاء
AnimatedContent
ضمنSharedTransitionLayout
. يتم تمرير النطاقات منSharedTransitionLayout
وAnimatedContent
إلىMainContent
وDetailsContent
على النحو التالي:var showDetails by remember { mutableStateOf(false) } SharedTransitionLayout { AnimatedContent( showDetails, label = "basic_transition" ) { targetState -> if (!targetState) { MainContent( onShowDetails = { showDetails = true }, animatedVisibilityScope = this@AnimatedContent, sharedTransitionScope = this@SharedTransitionLayout ) } else { DetailsContent( onBack = { showDetails = false }, animatedVisibilityScope = this@AnimatedContent, sharedTransitionScope = this@SharedTransitionLayout ) } } }
أضِف
Modifier.sharedElement()
إلى سلسلة المعدِّلات القابلة للإنشاء في العنصرَين القابلَين للإنشاء المتطابقَين. أنشئ عنصرSharedContentState
وتذكَّره باستخدامrememberSharedContentState()
. يخزّن الكائنSharedContentState
المفتاح الفريد الذي يحدّد العناصر التي تتم مشاركتها. قدِّم مفتاحًا فريدًا لتحديد المحتوى، واستخدِمrememberSharedContentState()
للعنصر الذي تريد تذكّره. يتم تمريرAnimatedContentScope
إلى المعدِّل الذي يُستخدَم لتنسيق الرسوم المتحركة.@Composable private fun MainContent( onShowDetails: () -> Unit, modifier: Modifier = Modifier, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { Row( // ... ) { with(sharedTransitionScope) { Image( painter = painterResource(id = R.drawable.cupcake), contentDescription = "Cupcake", modifier = Modifier .sharedElement( rememberSharedContentState(key = "image"), animatedVisibilityScope = animatedVisibilityScope ) .size(100.dp) .clip(CircleShape), contentScale = ContentScale.Crop ) // ... } } } @Composable private fun DetailsContent( modifier: Modifier = Modifier, onBack: () -> Unit, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { Column( // ... ) { with(sharedTransitionScope) { Image( painter = painterResource(id = R.drawable.cupcake), contentDescription = "Cupcake", modifier = Modifier .sharedElement( rememberSharedContentState(key = "image"), animatedVisibilityScope = animatedVisibilityScope ) .size(200.dp) .clip(CircleShape), contentScale = ContentScale.Crop ) // ... } } }
للحصول على معلومات حول ما إذا حدثت مطابقة لعنصر مشترك، استخرِج
rememberSharedContentState()
إلى متغيّر، ثم استعلم عن isMatchFound
.
يؤدي ذلك إلى إنشاء الرسوم المتحركة التلقائية التالية:

قد تلاحظ أنّ لون الخلفية وحجم الحاوية بأكملها لا يزالان يستخدمان إعدادات AnimatedContent
التلقائية.
الحدود المشتركة مقابل العنصر المشترك
Modifier.sharedBounds()
مشابهة لـ Modifier.sharedElement()
.
ومع ذلك، تختلف المعدّلات في ما يلي:
- يُستخدم
sharedBounds()
للمحتوى الذي يختلف ظاهريًا ولكن يجب أن يتشارك المنطقة نفسها بين الولايات، بينما يتوقّعsharedElement()
أن يكون المحتوى متطابقًا. - باستخدام
sharedBounds()
، يظهر المحتوى الذي يدخل الشاشة ويخرج منها أثناء الانتقال بين الحالتين، بينما باستخدامsharedElement()
، يتم عرض المحتوى المستهدف فقط في الحدود المتحوّلة. تحتويModifier.sharedBounds()
على المَعلمتَينenter
وexit
لتحديد طريقة انتقال المحتوى، على غرار طريقة عملAnimatedContent
. - تتمثّل حالة الاستخدام الأكثر شيوعًا لـ
sharedBounds()
في نمط تحويل الحاوية، بينما تتمثّل حالة الاستخدام المثال لـsharedElement()
في انتقال العنصر البارز. - عند استخدام عناصر
Text
القابلة للإنشاء، يُفضَّل استخدامsharedBounds()
لتوفير إمكانية تغيير الخط، مثل الانتقال بين الخط المائل والخط الغامق أو تغيير الألوان.
في المثال السابق، ستتيح لنا إضافة Modifier.sharedBounds()
إلى Row
وColumn
في السيناريوهَين المختلفَين مشاركة حدود العنصرَين وتنفيذ الرسوم المتحركة للانتقال، ما يسمح لهما بالتوسّع بين بعضهما البعض:
@Composable private fun MainContent( onShowDetails: () -> Unit, modifier: Modifier = Modifier, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { with(sharedTransitionScope) { Row( modifier = Modifier .padding(8.dp) .sharedBounds( rememberSharedContentState(key = "bounds"), animatedVisibilityScope = animatedVisibilityScope, enter = fadeIn(), exit = fadeOut(), resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() ) // ... ) { // ... } } } @Composable private fun DetailsContent( modifier: Modifier = Modifier, onBack: () -> Unit, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { with(sharedTransitionScope) { Column( modifier = Modifier .padding(top = 200.dp, start = 16.dp, end = 16.dp) .sharedBounds( rememberSharedContentState(key = "bounds"), animatedVisibilityScope = animatedVisibilityScope, enter = fadeIn(), exit = fadeOut(), resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() ) // ... ) { // ... } } }
التعرّف على النطاقات
لاستخدام Modifier.sharedElement()
، يجب أن يكون العنصر القابل للإنشاء ضمن SharedTransitionScope
. توفّر الدالة البرمجية القابلة للإنشاء SharedTransitionLayout
SharedTransitionScope
. احرص على وضعها في النقطة نفسها ذات المستوى الأعلى في التسلسل الهرمي لواجهة المستخدم التي تحتوي على العناصر التي تريد مشاركتها.
بشكل عام، يجب أيضًا وضع العناصر القابلة للإنشاء داخل AnimatedVisibilityScope
. يتم توفير ذلك عادةً باستخدام AnimatedContent
للتبديل بين العناصر القابلة للإنشاء أو عند استخدام AnimatedVisibility
مباشرةً، أو من خلال
الدالة القابلة للإنشاء NavHost
، ما لم تتم إدارة مستوى الظهور
يدويًا. لاستخدام نطاقات متعددة، احفظ النطاقات المطلوبة في CompositionLocal
، أو استخدِم مستقبِلات السياق في Kotlin، أو مرِّر النطاقات كمعلَمات إلى الدوال.
استخدِم CompositionLocals
في السيناريو الذي يتضمّن نطاقات متعدّدة لتتبُّعها، أو تسلسلاً هرميًا متداخلاً. تتيح لك CompositionLocal
اختيار النطاقات المحدّدة التي تريد حفظها واستخدامها. من ناحية أخرى، عند استخدام أدوات استقبال السياق، قد تؤدي التصميمات الأخرى في التسلسل الهرمي إلى إلغاء النطاقات المقدَّمة عن طريق الخطأ.
على سبيل المثال، إذا كان لديك عدة AnimatedContent
متداخلة، يمكن إلغاء النطاقات.
val LocalNavAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null } val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null } @Composable private fun SharedElementScope_CompositionLocal() { // An example of how to use composition locals to pass around the shared transition scope, far down your UI tree. // ... SharedTransitionLayout { CompositionLocalProvider( LocalSharedTransitionScope provides this ) { // This could also be your top-level NavHost as this provides an AnimatedContentScope AnimatedContent(state, label = "Top level AnimatedContent") { targetState -> CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this) { // Now we can access the scopes in any nested composables as follows: val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No SharedElementScope found") val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current ?: throw IllegalStateException("No AnimatedVisibility found") } // ... } } } }
بدلاً من ذلك، إذا لم يكن التسلسل الهرمي متداخلاً بشكل كبير، يمكنك تمرير النطاقات كوسيطات:
@Composable fun MainContent( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { } @Composable fun Details( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { }
العناصر المشترَكة مع "AnimatedVisibility
"
أوضحت الأمثلة السابقة كيفية استخدام العناصر المشترَكة مع AnimatedContent
، ولكن يمكن استخدام العناصر المشترَكة مع AnimatedVisibility
أيضًا.
على سبيل المثال، في هذا المثال الخاص بالشبكة الكسولة، يتم تضمين كل عنصر في
AnimatedVisibility
. عند النقر على العنصر، يظهر المحتوى بتأثير مرئي وكأنّه تم سحبه من واجهة المستخدم إلى مكوّن يشبه مربّع الحوار.
var selectedSnack by remember { mutableStateOf<Snack?>(null) } SharedTransitionLayout(modifier = Modifier.fillMaxSize()) { LazyColumn( // ... ) { items(listSnacks) { snack -> AnimatedVisibility( visible = snack != selectedSnack, enter = fadeIn() + scaleIn(), exit = fadeOut() + scaleOut(), modifier = Modifier.animateItem() ) { Box( modifier = Modifier .sharedBounds( sharedContentState = rememberSharedContentState(key = "${snack.name}-bounds"), // Using the scope provided by AnimatedVisibility animatedVisibilityScope = this, clipInOverlayDuringTransition = OverlayClip(shapeForSharedElement) ) .background(Color.White, shapeForSharedElement) .clip(shapeForSharedElement) ) { SnackContents( snack = snack, modifier = Modifier.sharedElement( sharedContentState = rememberSharedContentState(key = snack.name), animatedVisibilityScope = this@AnimatedVisibility ), onClick = { selectedSnack = snack } ) } } } } // Contains matching AnimatedContent with sharedBounds modifiers. SnackEditDetails( snack = selectedSnack, onConfirmClick = { selectedSnack = null } ) }
AnimatedVisibility
ترتيب المعدِّلات
في Modifier.sharedElement()
وModifier.sharedBounds()
، يهم ترتيب سلسلة
المعدِّلات،
كما هو الحال مع بقية Compose. يمكن أن يؤدي الموضع غير الصحيح للمعدِّلات التي تؤثر في الحجم إلى حدوث قفزات مرئية غير متوقّعة أثناء مطابقة العناصر المشتركة.
على سبيل المثال، إذا وضعت أداة تعديل المساحة المتروكة في موضع مختلف على عنصرَين مشترَكَين، سيظهر اختلاف مرئي في الحركة.
var selectFirst by remember { mutableStateOf(true) } val key = remember { Any() } SharedTransitionLayout( Modifier .fillMaxSize() .padding(10.dp) .clickable { selectFirst = !selectFirst } ) { AnimatedContent(targetState = selectFirst, label = "AnimatedContent") { targetState -> if (targetState) { Box( Modifier .padding(12.dp) .sharedBounds( rememberSharedContentState(key = key), animatedVisibilityScope = this@AnimatedContent ) .border(2.dp, Color.Red) ) { Text( "Hello", fontSize = 20.sp ) } } else { Box( Modifier .offset(180.dp, 180.dp) .sharedBounds( rememberSharedContentState( key = key, ), animatedVisibilityScope = this@AnimatedContent ) .border(2.dp, Color.Red) // This padding is placed after sharedBounds, but it doesn't match the // other shared elements modifier order, resulting in visual jumps .padding(12.dp) ) { Text( "Hello", fontSize = 36.sp ) } } } }
الحدود المطابقة |
حدود غير متطابقة: لاحظ كيف تظهر الصورة المتحركة للعنصر المشترَك بشكل غير صحيح لأنّه يجب تغيير حجمها لتناسب الحدود غير الصحيحة. |
---|---|
توفّر المعدِّلات المستخدَمة قبل معدِّلات العناصر المشترَكة قيودًا على معدِّلات العناصر المشترَكة، والتي تُستخدَم بعد ذلك لاشتقاق الحدود الأولية وحدود الهدف، ثم حدود الحركة.
تستخدِم المعدِّلات المستخدَمة بعد معدِّلات العناصر المشترَكة القيود السابقة لقياس حجم العنصر التابع المستهدَف واحتسابه. تنشئ معدِّلات العناصر المشترَكة سلسلة من القيود المتحركة لتحويل العنصر الفرعي تدريجيًا من الحجم الأولي إلى الحجم المستهدَف.
الاستثناء الوحيد هو إذا كنت تستخدم resizeMode = ScaleToBounds()
للرسوم المتحركة أو Modifier.skipToLookaheadSize()
في عنصر قابل للإنشاء. في هذه الحالة، يحدّد Compose موضع العنصر التابع باستخدام القيود المستهدَفة، ويستخدم بدلاً من ذلك عامل مقياس لتنفيذ الحركة بدلاً من تغيير حجم التصميم نفسه.
المفاتيح الفريدة
عند التعامل مع عناصر مشترَكة معقّدة، من الممارسات الجيدة إنشاء مفتاح ليس سلسلة، لأنّ السلاسل قد تكون عرضة للخطأ في المطابقة. يجب أن يكون كل مفتاح فريدًا حتى تحدث المطابقات. على سبيل المثال، في تطبيق Jetsnack، لدينا العناصر المشترَكة التالية:

يمكنك إنشاء تعداد لتحديد نوع العنصر المشترَك. في هذا المثال، يمكن أن تظهر بطاقة الوجبة السريعة الكاملة أيضًا من عدة أماكن مختلفة على الشاشة الرئيسية، مثلاً في قسمَي "المحتوى الرائج" و "المحتوى المقترَح". يمكنك إنشاء مفتاح يتضمّن snackId
وorigin
("الأكثر رواجًا" أو "مقترَحة") وtype
للعنصر المشترَك الذي ستتم مشاركته:
data class SnackSharedElementKey( val snackId: Long, val origin: String, val type: SnackSharedElementType ) enum class SnackSharedElementType { Bounds, Image, Title, Tagline, Background } @Composable fun SharedElementUniqueKey() { // ... Box( modifier = Modifier .sharedElement( rememberSharedContentState( key = SnackSharedElementKey( snackId = 1, origin = "latest", type = SnackSharedElementType.Image ) ), animatedVisibilityScope = this@AnimatedVisibility ) ) // ... }
ننصح باستخدام فئات البيانات للمفاتيح لأنّها تنفّذ hashCode()
وisEquals()
.
إدارة إمكانية ظهور العناصر المشتركة يدويًا
في الحالات التي قد لا تستخدم فيها AnimatedVisibility
أو AnimatedContent
، يمكنك إدارة إذن الوصول إلى العنصر المشترَك بنفسك. استخدِم
Modifier.sharedElementWithCallerManagedVisibility()
وقدِّم الشرط الخاص بك الذي يحدّد ما إذا كان يجب أن يكون العنصر مرئيًا أم لا:
var selectFirst by remember { mutableStateOf(true) } val key = remember { Any() } SharedTransitionLayout( Modifier .fillMaxSize() .padding(10.dp) .clickable { selectFirst = !selectFirst } ) { Box( Modifier .sharedElementWithCallerManagedVisibility( rememberSharedContentState(key = key), !selectFirst ) .background(Color.Red) .size(100.dp) ) { Text(if (!selectFirst) "false" else "true", color = Color.White) } Box( Modifier .offset(180.dp, 180.dp) .sharedElementWithCallerManagedVisibility( rememberSharedContentState( key = key, ), selectFirst ) .alpha(0.5f) .background(Color.Blue) .size(180.dp) ) { Text(if (selectFirst) "false" else "true", color = Color.White) } }
القيود الحالية
لهذه الواجهات بعض القيود. أبرزها:
- لا تتوفّر إمكانية التشغيل التفاعلي بين Views وCompose. ويشمل ذلك أي عنصر قابل للإنشاء يلتف حول
AndroidView
، مثلDialog
أوModalBottomSheet
. - لا تتوفّر ميزة إنشاء صور متحركة تلقائيًا للعناصر التالية:
- عناصر Shared Image القابلة للإنشاء:
- لا يتم تحريك
ContentScale
تلقائيًا. يتم ضبطه على وقت الانتهاء المحدّدContentScale
.
- لا يتم تحريك
- اقتصاص الأشكال: لا تتوفّر ميزة مدمجة لتفعيل الرسوم المتحركة تلقائيًا بين الأشكال، مثلاً، الانتقال من شكل مربّع إلى شكل دائري أثناء انتقال العنصر.
- في الحالات غير المتوافقة، استخدِم
Modifier.sharedBounds()
بدلاً منsharedElement()
وأضِفModifier.animateEnterExit()
إلى العناصر.
- عناصر Shared Image القابلة للإنشاء: