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