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