تُعد عمليات الانتقال بين العناصر المشترَكة طريقة سلسة للانتقال بين عناصر قابلة للإنشاء تتضمّن محتوًى متطابقًا. ويتم استخدامها غالبًا للتنقّل، ما يتيح لك ربط شاشات مختلفة بشكل مرئي أثناء تنقّل المستخدم بينها.
على سبيل المثال، في الفيديو التالي، يمكنك الاطّلاع على الصورة والعنوان الخاصين بالوجبة السريعة، وهما مشارَكان من صفحة البيانات إلى صفحة التفاصيل.
في 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() وequals().
إدارة إمكانية ظهور العناصر المشترَكة يدويًا
في الحالات التي قد لا تستخدم فيها 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 القابلة للإنشاء: