शेयर किए गए एलिमेंट के ट्रांज़िशन की मदद से, एक जैसे कॉन्टेंट वाले कॉम्पोज़ेबल के बीच आसानी से ट्रांज़िशन किया जा सकता है. आम तौर पर, नेविगेशन के लिए इनका इस्तेमाल किया जाता है. इससे उपयोगकर्ता इन स्क्रीन पर नेविगेट करते समय अलग-अलग स्क्रीन को विज़ुअल तौर पर कनेक्ट कर सकते हैं.
उदाहरण के लिए, इस वीडियो में, लिस्टिंग पेज से लेकर ज़्यादा जानकारी वाले पेज तक, शेयर किए गए स्नैक की इमेज और टाइटल देखा जा सकता है.
Compose में कुछ हाई लेवल एपीआई हैं जो शेयर किए जाने वाले एलिमेंट बनाने में आपकी मदद करते हैं:
SharedTransitionLayout
: शेयर किए गए एलिमेंट ट्रांज़िशन को लागू करने के लिए, सबसे बाहरी लेआउट की ज़रूरत होती है. इससेSharedTransitionScope
मिलता है. शेयर किए गए एलिमेंट के मॉडिफ़ायर का इस्तेमाल करने के लिए, कॉम्पोज़ेबल कोSharedTransitionScope
में होना चाहिए.Modifier.sharedElement()
: यह एक ऐसा मॉडिफ़ायर है जोSharedTransitionScope
उस कॉम्पोज़ेबल को फ़्लैग करता है जिसे किसी दूसरे कॉम्पोज़ेबल से मैच करना है.Modifier.sharedBounds()
: वह मॉडिफ़ायर जोSharedTransitionScope
को फ़्लैग करता है, तो इस कंपोज़ेबल के बाउंड का इस्तेमाल कंटेनर बाउंड के तौर पर किया जाना चाहिए. इससे, यह तय किया जा सकेगा कि ट्रांज़िशन कहां होना चाहिए.sharedElement()
के मुकाबले,sharedBounds()
को अलग-अलग तरह के विज़ुअल वाले कॉन्टेंट के लिए डिज़ाइन किया गया है.
कंपोज़ में शेयर किए गए एलिमेंट बनाते समय एक अहम बात यह है कि वे ओवरले और क्लिपिंग के साथ कैसे काम करते हैं. इस अहम विषय के बारे में ज़्यादा जानने के लिए, क्लिपिंग और ओवरले सेक्शन पर नज़र डालें.
बुनियादी इस्तेमाल
इस सेक्शन में, नीचे दिया गया ट्रांज़िशन बनाया जाएगा. इसमें छोटे "सूची" आइटम से बड़े और ज़्यादा जानकारी वाले आइटम पर ट्रांज़िशन किया जाएगा:

Modifier.sharedElement()
का इस्तेमाल करने का सबसे अच्छा तरीका AnimatedContent
, AnimatedVisibility
या NavHost
के साथ इस्तेमाल करना है, क्योंकि इससे कंपोज़ेबल के बीच ट्रांज़िशन को अपने-आप मैनेज किया जा सकता है.
शुरुआत में, शेयर किए गए एलिमेंट जोड़ने से पहले, एक मौजूदा बुनियादी AnimatedContent
होता है, जिसमें MainContent
और DetailsContent
कॉम्पोज़ेबल होता है:

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