إنّ انتقالات العناصر المشترَكة هي طريقة سلسة للانتقال بين العناصر القابلة للتجميع التي تتضمّن محتوى متسقًا بينها. ويتم استخدامها غالبًا للتنقّل، ما يتيح لك ربط الشاشات المختلفة بشكل مرئي أثناء تنقّل المستخدمين بينها.
على سبيل المثال، في الفيديو التالي، يمكنك ملاحظة مشاركة صورة وعنوان الوجبة الخفيفة من صفحة البيانات إلى صفحة التفاصيل.
في Compose، تتوفّر بعض واجهات برمجة التطبيقات ذات المستوى العالي لمساعدتك في إنشاء عناصر مشترَكة:
SharedTransitionLayout
: التصميم الخارجي المطلوب لتنفيذ عمليات نقل العناصر المشتركة. يوفّرSharedTransitionScope
. يجب أن تكون العناصر القابلة للتجميع فيSharedTransitionScope
لاستخدام عوامل تعديل العناصر المشترَكة.Modifier.sharedElement()
: عنصر التعديل الذي يضع إشارة إلىSharedTransitionScope
العنصر القابل للإنشاء الذي يجب مطابقته مع عنصر آخر قابل للإنشاء.Modifier.sharedBounds()
: المُعدِّل الذي يُعلمSharedTransitionScope
بأنّه يجب استخدام حدود هذا المكوّن القابل للتجميع كحدود الحاوية التي يجب أن يحدث فيها الانتقال على عكسsharedElement()
، تم تصميمsharedBounds()
للمحتوى الذي يختلف من حيث الشكل.
من المفاهيم المهمة عند إنشاء عناصر مشترَكة في "الإنشاء" هي كيفية عملها مع العناصر التي تظهر على سطح الشاشة والعناصر التي تم اقتصاصها. اطّلِع على قسم الاقتصاص والتراكبات للتعرّف على مزيد من المعلومات حول هذا الموضوع المهم.
الاستخدام الأساسي
سيتم إنشاء الانتقال التالي في هذا القسم، للانتقال من عنصر "القائمة" الأصغر، إلى العنصر التفصيلي الأكبر:
وأفضل طريقة لاستخدام علامة Modifier.sharedElement()
هي أن تستخدم الترميز
AnimatedContent
أو AnimatedVisibility
أو NavHost
لأنّها تدير
عملية الانتقال بين العناصر القابلة للإنشاء تلقائيًا نيابةً عنك.
نقطة البداية هي AnimatedContent
أساسي حالي يحتوي على
MainContent
وDetailsContent
قابلَين للتركيب قبل إضافة عناصر مشترَكة:
لإضافة تأثيرات متحركة إلى العناصر المشتركة بين التنسيقَين، احط
SharedTransitionLayout
حول العنصر القابل للتجميعAnimatedContent
. يتم تمرير النطاقات من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( state = rememberSharedContentState(key = snack.name), animatedVisibilityScope = this@AnimatedVisibility ), onClick = { selectedSnack = snack } ) } } } } // Contains matching AnimatedContent with sharedBounds modifiers. SnackEditDetails( snack = selectedSnack, onConfirmClick = { selectedSnack = null } ) }
ترتيب المُعدِّلات
مع 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) } }
القيود الحالية
تفرض واجهات برمجة التطبيقات هذه بعض القيود. في ما يلي أبرز التغييرات:
- لا تتوفّر إمكانية التشغيل التفاعلي بين "الاطِّلاع" و"الإنشاء". ويشمل ذلك
أي عنصر قابل للتجميع يلف
AndroidView
، مثلDialog
. - لا تتوفّر ميزة الصور المتحركة التلقائية في ما يلي:
- العناصر القابلة للتجميع من الصور المشتركة:
- لا تكون السمة
ContentScale
متحركة تلقائيًا. يتم تثبيته على النهاية المحدّدةContentScale
.
- لا تكون السمة
- اقتصاص الأشكال: لا تتوفّر ميزة مدمجة لإضافة تأثيرات متحركة تلقائية بين الأشكال، مثل إضافة تأثير متحركة من مربّع إلى دائرة أثناء انتقال العنصر.
- بالنسبة إلى الحالات غير المتوافقة، استخدِم
Modifier.sharedBounds()
بدلاً منsharedElement()
وأضِفModifier.animateEnterExit()
إلى العناصر.
- العناصر القابلة للتجميع من الصور المشتركة: