انتقال عناصر مشترک یک راه بدون درز برای انتقال بین اجزای ترکیبی است که دارای محتوایی است که بین آنها سازگار است. آنها اغلب برای ناوبری استفاده می شوند و به شما این امکان را می دهند که به صورت بصری صفحه های مختلف را در حین حرکت کاربر بین آنها متصل کنید.
به عنوان مثال، در ویدیوی زیر، می توانید مشاهده کنید که تصویر و عنوان میان وعده از صفحه لیست، به صفحه جزئیات به اشتراک گذاشته شده است.
در Compose، چند API سطح بالا وجود دارد که به شما در ایجاد عناصر مشترک کمک می کند:
-
SharedTransitionLayout
: بیرونی ترین طرح مورد نیاز برای پیاده سازی انتقال عناصر مشترک. این یکSharedTransitionScope
ارائه می دهد. Composable ها باید درSharedTransitionScope
باشند تا از اصلاح کننده های عنصر مشترک استفاده کنند. -
Modifier.sharedElement()
: اصلاحکنندهای که ترکیبپذیری را که باید با یک composable دیگر مطابقت داده شود، بهSharedTransitionScope
پرچمگذاری میکند. -
Modifier.sharedBounds()
: اصلاح کننده ای که بهSharedTransitionScope
علامت گذاری می کند که کرانه های این composable باید به عنوان کران کانتینر برای جایی که انتقال باید انجام شود استفاده می شود. بر خلافsharedElement()
،sharedBounds()
برای محتوای بصری متفاوت طراحی شده است.
یک مفهوم مهم هنگام ایجاد عناصر مشترک در Compose نحوه کار آنها با همپوشانی و برش است. برای کسب اطلاعات بیشتر در مورد این موضوع مهم، به بخش بریده و همپوشانی نگاهی بیندازید.
استفاده پایه
انتقال زیر در این بخش ساخته خواهد شد و از آیتم "فهرست" کوچکتر به آیتم با جزئیات بزرگتر منتقل می شود:
بهترین راه برای استفاده از Modifier.sharedElement()
در ارتباط با AnimatedContent
، AnimatedVisibility
یا NavHost
است زیرا این انتقال بین composable ها را به طور خودکار برای شما مدیریت می کند.
نقطه شروع یک AnimatedContent
اساسی موجود است که دارای MainContent
و DetailsContent
قابل ترکیب قبل از افزودن عناصر مشترک است:
برای اینکه عناصر مشترک بین دو طرحبندی متحرک شوند،
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()
به زنجیره اصلاح کننده قابل ترکیب خود در دو composable که مطابقت دارند اضافه کنید. یک شی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
composables،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() ) // ... ) { // ... } } }
Scopes را درک کنید
برای استفاده از Modifier.sharedElement()
، composable باید در SharedTransitionScope
باشد. Composable SharedTransitionLayout
SharedTransitionScope
را ارائه می دهد. مطمئن شوید که در همان نقطه سطح بالایی در سلسله مراتب رابط کاربری خود قرار دهید که حاوی عناصری است که می خواهید به اشتراک بگذارید.
به طور کلی، composable ها نیز باید در داخل 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 اهمیت دارد. قرارگیری نادرست اصلاحکنندههای تأثیرگذار بر اندازه میتواند باعث پرشهای بصری غیرمنتظره در طول تطبیق عناصر مشترک شود.
به عنوان مثال، اگر یک اصلاح کننده padding را در موقعیت متفاوتی روی دو عنصر مشترک قرار دهید، تفاوت بصری در انیمیشن وجود دارد.
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()
در یک composable استفاده کنید. در این حالت، Compose با استفاده از محدودیتهای هدف، کودک را دراز میکند و در عوض از یک ضریب مقیاس برای اجرای انیمیشن به جای تغییر اندازه طرحبندی استفاده میکند.
کلیدهای منحصر به فرد
هنگام کار با عناصر به اشتراک گذاشته شده پیچیده، ایجاد کلیدی که رشته ای نباشد، تمرین خوبی است، زیرا رشته ها ممکن است مستعد خطا باشند. هر کلید باید منحصر به فرد باشد تا مطابقت اتفاق بیفتد. به عنوان مثال، در Jetsnack ما عناصر مشترک زیر را داریم:
می توانید یک enum برای نشان دادن نوع عنصر مشترک ایجاد کنید. در این مثال کل کارت میان وعده نیز می تواند از چندین مکان مختلف در صفحه اصلی ظاهر شود، به عنوان مثال در بخش «محبوب» و «توصیه شده». میتوانید کلیدی ایجاد کنید که دارای 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) } }
محدودیت های فعلی
این API ها دارای چند محدودیت هستند. مهمترین موارد:
- هیچ قابلیت همکاری بین Views و Compose پشتیبانی نمیشود. این شامل هر ترکیبی است که
AndroidView
را میپیچد، مانندDialog
. - هیچ پشتیبانی خودکار انیمیشن برای موارد زیر وجود ندارد:
- ترکیبات تصویر مشترک :
-
ContentScale
به طور پیش فرض متحرک نیست. به مجموعهContentScale
میچسبد.
-
- برش شکل - هیچ پشتیبانی داخلی برای پویانمایی خودکار بین اشکال وجود ندارد - برای مثال متحرک سازی از یک مربع به یک دایره در حین انتقال آیتم.
- برای موارد پشتیبانی نشده، به جای
sharedElement()
ازModifier.sharedBounds()
استفاده کنید وModifier.animateEnterExit()
به موارد اضافه کنید.
- ترکیبات تصویر مشترک :