انتقالهای عنصر مشترک روشی یکپارچه برای انتقال بین composableهایی است که محتوای سازگار بین آنها وجود دارد. اغلب برای پیمایش استفاده میشوند و به شما امکان میدهند صفحههای مختلف را بهصورت بصری به هم متصل کنید تا کاربر بتواند بین آنها پیمایش کند.
برای مثال، در ویدیو زیر، میتوانید ببینید که تصویر و عنوان تنقلات از صفحه فهرستگان به صفحه جزئیات همرسانی شده است.
در Compose، چند میانای برنامهسازی کاربردی سطح بالا وجود دارد که به شما کمک میکند عناصر مشترک ایجاد کنید:
SharedTransitionLayout: بیرونیترین چیدمان موردنیاز برای پیادهسازی انتقالهای عنصر همرسانیشده. این بخشSharedTransitionScopeارائه میدهد. برای استفاده از اصلاحکنندههای عنصر مشترک، ترکیبپذیرها باید درSharedTransitionScopeباشند.Modifier.sharedElement(): اصلاحکنندهای که بهSharedTransitionScopeتابع ترکیبپذیری که باید با تابع ترکیبپذیر دیگری مطابقت داده شود پرچم میزند.Modifier.sharedBounds(): اصلاحکنندهای که بهSharedTransitionScopeنشان میدهد که مرزهای این ترکیبپذیر باید بهعنوان مرزهای محفظه برای جایی که انتقال باید انجام شود استفاده شود. برخلافsharedElement()،sharedBounds()برای محتوای متفاوت ازنظر بصری طراحی شده است.
مفهوم مهمی که هنگام ایجاد عناصر مشترک در Compose باید درنظر گرفت نحوه کار آنها با همپوشانیها و برش است. برای کسب اطلاعات بیشتر درباره این موضوع مهم، به بخش برش و همپوشانی مراجعه کنید.
استفاده پایه
گذار زیر در این بخش ساخته خواهد شد، گذار از مورد «فهرست» کوچکتر به مورد تفصیلی بزرگتر:
بهترین روش استفاده از Modifier.sharedElement() همراه با
AnimatedContent، AnimatedVisibility، یا NavHost است، زیرا این روش
انتقال بین composable را بهطور خودکار برای شما مدیریت میکند.
نقطه شروع یک 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()، مورد استفاده نمونه، انتقال قهرمان است. - هنگام استفاده از
Textcomposables،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()، composable باید در
SharedTransitionScope باشد. تابع ترکیبپذیر 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( 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 عناصر مشترک زیر را داریم:
میتوانید یک 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() و
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. - پویانمایی خودکار برای موارد زیر پشتیبانی نمیشود:
- ترکیبپذیرهای تصویر مشترک:
ContentScaleبهطور پیشفرض پویانمایی نمیشود. به انتهای تنظیمشده میچسبدContentScale.
- برش شکل - پشتیبانی داخلی برای پویانمایی خودکار بین شکلها وجود ندارد - برای مثال، پویانمایی از مربع به دایره هنگام انتقال مورد.
- برای موارد پشتیبانینشده، بهجای
sharedElement()ازModifier.sharedBounds()استفاده کنید وModifier.animateEnterExit()را به موارد اضافه کنید.
- ترکیبپذیرهای تصویر مشترک: