शेयर किए गए एलिमेंट ट्रांज़िशन, कंपोज़ेबल के बीच ट्रांज़िशन करने का एक आसान तरीका है. इनमें ऐसा कॉन्टेंट होता है जो उनके बीच एक जैसा होता है. इनका इस्तेमाल अक्सर नेविगेशन के लिए किया जाता है. इससे, उपयोगकर्ता के नेविगेट करने पर, अलग-अलग स्क्रीन को विज़ुअल तौर पर कनेक्ट किया जा सकता है.
उदाहरण के लिए, यहां दिए गए वीडियो में, स्नैक की इमेज और टाइटल को लिस्टिंग पेज से ज़्यादा जानकारी वाले पेज पर शेयर किया गया है.
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()का इस्तेमाल करना बेहतर होता है. जैसे, इटैलिक और बोल्ड के बीच ट्रांज़िशन करना या रंग बदलना.
पिछले उदाहरण में, दो अलग-अलग स्थितियों में Row और Column पर Modifier.sharedBounds() जोड़ने से, हमें दोनों की सीमाओं को शेयर करने और ट्रांज़िशन ऐनिमेशन को लागू करने की सुविधा मिलेगी. इससे, दोनों के बीच में बढ़ोतरी हो सकेगी:
@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 में हमारे पास ये शेयर किए गए एलिमेंट हैं:
शेयर किए गए एलिमेंट टाइप को दिखाने के लिए, एक 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) } }
मौजूदा सीमाएं
इन एपीआई की कुछ सीमाएं हैं. खास तौर पर:
- व्यू और Compose के बीच इंटरऑपरेबिलिटी की सुविधा काम नहीं करती. इसमें
AndroidViewको रैप करने वाला कोई भी कंपोज़ेबल शामिल है, जैसे किDialogयाModalBottomSheet. - इनके लिए, अपने-आप ऐनिमेशन बनने की सुविधा उपलब्ध नहीं है:
- शेयर की गई इमेज के कंपोज़ेबल:
ContentScaleडिफ़ॉल्ट रूप से ऐनिमेट नहीं होता. यह सेट किए गए आखिरी हिस्से पर स्नैप हो जाता हैContentScale.
- शेप क्लिपिंग - अलग-अलग शेप के बीच अपने-आप ऐनिमेशन होने की सुविधा पहले से मौजूद नहीं है. उदाहरण के लिए, आइटम के ट्रांज़िशन के दौरान, स्क्वेयर से सर्कल में ऐनिमेशन होना.
- जिन मामलों में यह सुविधा काम नहीं करती उनके लिए,
sharedElement()के बजायModifier.sharedBounds()का इस्तेमाल करें. साथ ही, आइटम मेंModifier.animateEnterExit()जोड़ें.
- शेयर की गई इमेज के कंपोज़ेबल: