শেয়ার্ড এলিমেন্ট ট্রানজিশন হল কম্পোজেবলের মধ্যে ট্রানজিশনের একটি মসৃণ উপায় যার মধ্যে সামঞ্জস্যপূর্ণ কন্টেন্ট থাকে। এগুলি প্রায়শই নেভিগেশনের জন্য ব্যবহৃত হয়, যা আপনাকে ব্যবহারকারীর মধ্যে নেভিগেট করার সময় বিভিন্ন স্ক্রিনকে দৃশ্যত সংযুক্ত করার অনুমতি দেয়।
উদাহরণস্বরূপ, নিম্নলিখিত ভিডিওতে, আপনি দেখতে পাচ্ছেন যে খাবারের ছবি এবং শিরোনাম তালিকা পৃষ্ঠা থেকে বিস্তারিত পৃষ্ঠায় ভাগ করা হয়েছে।
কম্পোজে, কয়েকটি উচ্চ স্তরের API রয়েছে যা আপনাকে ভাগ করা উপাদান তৈরি করতে সহায়তা করে:
-
SharedTransitionLayout: শেয়ার্ড এলিমেন্ট ট্রানজিশন বাস্তবায়নের জন্য প্রয়োজনীয় সবচেয়ে বাইরের লেআউট। এটি একটিSharedTransitionScopeপ্রদান করে। শেয়ার্ড এলিমেন্ট মডিফায়ার ব্যবহার করার জন্য কম্পোজেবলগুলিকে একটিSharedTransitionScopeএ থাকতে হবে। -
Modifier.sharedElement(): যে মডিফায়ারটিSharedTransitionScopeএ কম্পোজেবলকে ফ্ল্যাগ করে এবং অন্য কম্পোজেবলের সাথে মেলানো উচিত। -
Modifier.sharedBounds(): যে মডিফায়ারটিSharedTransitionScopeএ ফ্ল্যাগ করে যে এই কম্পোজেবলের সীমানাটি ট্রানজিশনটি কোথায় হওয়া উচিত তার কন্টেইনার সীমানা হিসেবে ব্যবহার করা উচিত।sharedElement()এর বিপরীতে,sharedBounds()দৃশ্যত ভিন্ন কন্টেন্টের জন্য ডিজাইন করা হয়েছে।
কম্পোজে শেয়ার করা এলিমেন্ট তৈরি করার সময় একটি গুরুত্বপূর্ণ ধারণা হল ওভারলে এবং ক্লিপিং কীভাবে কাজ করে। এই গুরুত্বপূর্ণ বিষয়টি সম্পর্কে আরও জানতে ক্লিপিং এবং ওভারলে বিভাগটি দেখুন।
মৌলিক ব্যবহার
এই বিভাগে নিম্নলিখিত রূপান্তরটি তৈরি করা হবে, ছোট "তালিকা" আইটেম থেকে বৃহত্তর বিস্তারিত আইটেমে রূপান্তরিত হবে:

Modifier.sharedElement() ব্যবহার করার সর্বোত্তম উপায় হল AnimatedContent , AnimatedVisibility , অথবা NavHost সাথে একত্রে ব্যবহার করা, কারণ এটি আপনার জন্য স্বয়ংক্রিয়ভাবে কম্পোজেবলের মধ্যে রূপান্তর পরিচালনা করে।
শুরুর বিন্দু হল একটি বিদ্যমান মৌলিক AnimatedContent যার একটি MainContent এবং DetailsContent ভাগ করা উপাদান যোগ করার আগে কম্পোজেবল: 
AnimatedContent শুরু করা।
দুটি লেআউটের মধ্যে শেয়ার করা এলিমেন্টগুলিকে অ্যানিমেট করার জন্য,
AnimatedContentSharedTransitionLayoutদিয়ে কম্পোজেবল করে ঘিরে রাখুন।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 প্রদান করে। আপনার UI হায়ারার্কিতে একই শীর্ষ-স্তরের বিন্দুতে স্থাপন করতে ভুলবেন না যেখানে আপনি যে উপাদানগুলি ভাগ করতে চান তা রয়েছে।
সাধারণত, কম্পোজেবলগুলিকে একটি AnimatedVisibilityScope ভিতরেও স্থাপন করা উচিত। এটি সাধারণত কম্পোজেবলগুলির মধ্যে স্যুইচ করার জন্য AnimatedContent ব্যবহার করে অথবা সরাসরি AnimatedVisibility ব্যবহার করার সময়, অথবা NavHost composable ফাংশন দ্বারা প্রদান করা হয়, যদি না আপনি ম্যানুয়ালি দৃশ্যমানতা পরিচালনা করেন । একাধিক স্কোপ ব্যবহার করার জন্য, আপনার প্রয়োজনীয় স্কোপগুলিকে 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 তে মোড়ানো হয়। যখন আইটেমটিতে ক্লিক করা হয়, তখন বিষয়বস্তুটি UI থেকে একটি ডায়ালগ-সদৃশ উপাদানে টেনে আনার মতো দৃশ্যমান প্রভাব ফেলে।
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() ব্যবহার করেন, অথবা একটি composable-এ Modifier.skipToLookaheadSize() ব্যবহার করেন। এই ক্ষেত্রে, Compose টার্গেট সীমাবদ্ধতা ব্যবহার করে শিশুটিকে লেআউট করে, এবং পরিবর্তে লেআউটের আকার পরিবর্তন করার পরিবর্তে অ্যানিমেশন সম্পাদনের জন্য একটি স্কেল ফ্যাক্টর ব্যবহার করে।
অনন্য কী
জটিল শেয়ার্ড এলিমেন্টের সাথে কাজ করার সময়, এমন একটি কী তৈরি করা ভালো যা স্ট্রিং নয়, কারণ স্ট্রিংগুলি ত্রুটিপূর্ণভাবে মিলিত হতে পারে। মিলগুলি ঘটতে প্রতিটি কী অনন্য হতে হবে। উদাহরণস্বরূপ, জেটসন্যাক-এ আমাদের নিম্নলিখিত শেয়ার্ড এলিমেন্ট রয়েছে:

আপনি শেয়ার্ড এলিমেন্টের ধরণ উপস্থাপন করার জন্য একটি 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() এবং 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) } }
বর্তমান সীমাবদ্ধতা
এই API গুলির কিছু সীমাবদ্ধতা রয়েছে। সবচেয়ে উল্লেখযোগ্য:
- ভিউ এবং কম্পোজের মধ্যে কোনও আন্তঃকার্যক্ষমতা সমর্থিত নয়। এর মধ্যে রয়েছে যে কোনও কম্পোজেবল যা
AndroidViewমোড়ানো হয়, যেমন একটিDialogবাModalBottomSheet। - নিম্নলিখিতগুলির জন্য কোনও স্বয়ংক্রিয় অ্যানিমেশন সমর্থন নেই:
- শেয়ার করা ছবির কম্পোজেবল :
-
ContentScaleডিফল্টরূপে অ্যানিমেটেড হয় না। এটি সেট এন্ডContentScaleএ স্ন্যাপ করে।
-
- আকৃতি ক্লিপিং - আকারগুলির মধ্যে স্বয়ংক্রিয় অ্যানিমেশনের জন্য কোনও অন্তর্নির্মিত সমর্থন নেই - উদাহরণস্বরূপ, আইটেমটি রূপান্তরিত হওয়ার সাথে সাথে একটি বর্গক্ষেত্র থেকে একটি বৃত্তে অ্যানিমেট করা।
- অসমর্থিত ক্ষেত্রে,
sharedElement()এর পরিবর্তেModifier.sharedBounds()ব্যবহার করুন এবং আইটেমগুলিতেModifier.animateEnterExit()যোগ করুন।
- শেয়ার করা ছবির কম্পোজেবল :