انتقالهای عنصر مشترک روشی یکپارچه برای انتقال بین عناصر ترکیبی است که محتوای آنها با هم سازگار است. اینها اغلب برای پیمایش استفاده میشوند و به شما امکان میدهند صفحههای مختلف را بهصورت بصری به هم متصل کنید، زیرا کاربر بین آنها پیمایش میکند.
برای مثال، در ویدیو زیر، میتوانید ببینید که تصویر و عنوان میانوعده از صفحه فهرستگان به صفحه جزئیات همرسانی شده است.
در «نوشتن»، چند API سطح بالا وجود دارد که به شما کمک میکند عناصر مشترک ایجاد کنید:
SharedTransitionLayout: بیرونیترین چیدمانی که برای پیادهسازی انتقالهای عنصر مشترک لازم است.SharedTransitionScopeارائه میدهد. ترکیبپذیرها باید درSharedTransitionScopeباشند تا بتوانند از اصلاحکنندههای عنصر مشترک استفاده کنند.Modifier.sharedElement(): اصلاحگری که بهSharedTransitionScopeتابع ترکیبپذیری را علامتگذاری میکند که باید با تابع ترکیبپذیری دیگری مطابقت داده شود.Modifier.sharedBounds(): اصلاحگری که بهSharedTransitionScopeعلامت میدهد که مرزهای این عنصر ترکیبی باید بهعنوان مرزهای محتوی برای جایی که انتقال باید در آنجا انجام شود استفاده شود. برخلافsharedElement()،sharedBounds()برای محتوای متفاوت ازنظر بصری طراحی شده است.
مفهوم مهم هنگام ایجاد عناصر مشترک در «نوشتن» نحوه کار آنها با رونهادها و برش است. برای کسب اطلاعات بیشتر درباره این موضوع مهم، به بخش برش و رونهادها مراجعه کنید.
استفاده پایه
گذار زیر در این بخش ساخته خواهد شد، و از عنصر کوچکتر «فهرست» به عنصر بزرگتر با جزئیات بیشتر گذار خواهد کرد:
بهترین روش استفاده از 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()، ترتیب زنجیره
اصلاحگر اهمیت دارد،
همانند بقیه «نوشتن». جایگذاری نادرست اصلاحکنندههای تأثیرگذار بر اندازه
میتواند باعث پرشهای دیداری غیرمنتظره درطول تطبیق عنصر همرسانیشده شود.
برای مثال، اگر اصلاحگر بالشتک را در موقعیت متفاوتی روی دو عنصر مشترک قرار دهید، تفاوت بصری در پویانمایی وجود خواهد داشت.
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) } }
محدودیتهای فعلی
این «میاناهای برنامهسازی کاربردی» چند محدودیت دارند. بهطور قابلتوجه:
- از همکنشپذیری بین «نماها» و «نوشتن» پشتیبانی نمیشود. این شامل هر عنصر ترکیبیای میشود که
AndroidViewرا میپیچد، مثلDialogیاModalBottomSheet. - برای موارد زیر از پویانمایی خودکار پشتیبانی نمیشود:
- Shared Image composables:
-
ContentScaleبهطور پیشفرض پویانمایی نمیشود. به انتهای تنظیمشده میچسبدContentScale.
-
- برش شکل - پشتیبانی داخلی برای پویانمایی خودکار بین شکلها وجود ندارد - برای مثال، پویانمایی از مربع به دایره هنگام انتقال عنصر.
- برای موارد پشتیبانینشده، بهجای
sharedElement()ازModifier.sharedBounds()استفاده کنید وModifier.animateEnterExit()را به موارد اضافه کنید.
- Shared Image composables: