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