শেয়ার্ড এলিমেন্ট ট্রানজিশন হল কম্পোজেবলের মধ্যে ট্রানজিশন করার একটি বিরামহীন উপায় যেগুলির মধ্যে সামঞ্জস্যপূর্ণ বিষয়বস্তু রয়েছে। এগুলি প্রায়শই নেভিগেশনের জন্য ব্যবহার করা হয়, একজন ব্যবহারকারী তাদের মধ্যে নেভিগেট করার সময় আপনাকে বিভিন্ন স্ক্রীনকে দৃশ্যত সংযোগ করতে দেয়৷
উদাহরণস্বরূপ, নিম্নলিখিত ভিডিওতে, আপনি দেখতে পারেন যে স্ন্যাকটির চিত্র এবং শিরোনাম তালিকা পৃষ্ঠা থেকে বিশদ পৃষ্ঠায় ভাগ করা হয়েছে৷
রচনায়, কয়েকটি উচ্চ স্তরের API রয়েছে যা আপনাকে ভাগ করা উপাদানগুলি তৈরি করতে সহায়তা করে:
-
SharedTransitionLayout
: শেয়ার্ড এলিমেন্ট ট্রানজিশন বাস্তবায়নের জন্য প্রয়োজন সবচেয়ে বাইরের লেআউট। এটি একটিSharedTransitionScope
প্রদান করে। শেয়ার্ড এলিমেন্ট মডিফায়ার ব্যবহার করার জন্য কম্পোজেবলগুলিকে একটিSharedTransitionScope
এ থাকতে হবে। -
Modifier.sharedElement()
: সংশোধক যেটিSharedTransitionScope
কম্পোজেবলের সাথে ফ্ল্যাগ করে যা অন্য কম্পোজেবলের সাথে মিলিত হওয়া উচিত। -
Modifier.sharedBounds()
: যে সংশোধকটিSharedTransitionScope
এ পতাকাঙ্কিত করে যে এই কম্পোজেবলের সীমাগুলি যেখানে রূপান্তরটি ঘটতে হবে তার জন্য ধারক সীমা হিসাবে ব্যবহার করা উচিত।sharedElement()
এর বিপরীতে,sharedBounds()
দৃশ্যত ভিন্ন বিষয়বস্তুর জন্য ডিজাইন করা হয়েছে।
কম্পোজে ভাগ করা উপাদানগুলি তৈরি করার সময় একটি গুরুত্বপূর্ণ ধারণা হল তারা কীভাবে ওভারলে এবং ক্লিপিংয়ের সাথে কাজ করে। এই গুরুত্বপূর্ণ বিষয় সম্পর্কে আরও জানতে ক্লিপিং এবং ওভারলে বিভাগটি দেখুন।
মৌলিক ব্যবহার
ছোট "তালিকা" আইটেম থেকে বৃহত্তর বিস্তারিত আইটেমে রূপান্তর করে, এই বিভাগে নিম্নলিখিত রূপান্তর তৈরি করা হবে:
Modifier.sharedElement()
ব্যবহার করার সর্বোত্তম উপায় হল AnimatedContent
, AnimatedVisibility
বা NavHost
সাথে একত্রে কারণ এটি আপনার জন্য স্বয়ংক্রিয়ভাবে কম্পোজেবলের মধ্যে পরিবর্তন পরিচালনা করে।
প্রারম্ভিক বিন্দু হল একটি বিদ্যমান মৌলিক AnimatedContent
যার একটি MainContent
রয়েছে এবং ভাগ করা উপাদানগুলি যোগ করার আগে DetailsContent
কম্পোজযোগ্য:
শেয়ার্ড এলিমেন্ট দুটি লেআউটের মধ্যে অ্যানিমেট করার জন্য,
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()
AnimatedContent
কীভাবে কাজ করে তার অনুরূপ বিষয়বস্তু কীভাবে স্থানান্তরিত হবে তা নির্দিষ্ট করার জন্য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
সাথেও কাজ করে৷
উদাহরণস্বরূপ, এই অলস গ্রিডের উদাহরণে প্রতিটি উপাদান 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( 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()
এর সাথে আপনার সংশোধক চেইনের ক্রম গুরুত্বপূর্ণ, যেমনটি বাকি রচনাগুলির সাথে। সাইজ-এফেক্টিং মডিফায়ারের ভুল প্লেসমেন্ট শেয়ার করা এলিমেন্ট ম্যাচিং এর সময় অপ্রত্যাশিত ভিজ্যুয়াল জাম্প হতে পারে।
উদাহরণস্বরূপ, যদি আপনি দুটি ভাগ করা উপাদানে একটি প্যাডিং মডিফায়ারকে আলাদা অবস্থানে রাখেন, তবে অ্যানিমেশনে একটি ভিজ্যুয়াল পার্থক্য রয়েছে।
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()
ব্যবহার করেন। এই ক্ষেত্রে, কম্পোজ লক্ষ্য সীমাবদ্ধতা ব্যবহার করে শিশুকে সাজায়, এবং পরিবর্তে লেআউটের আকার পরিবর্তন করার পরিবর্তে অ্যানিমেশন সম্পাদন করার জন্য একটি স্কেল ফ্যাক্টর ব্যবহার করে।
অনন্য কী
জটিল ভাগ করা উপাদানগুলির সাথে কাজ করার সময়, একটি স্ট্রিং নয় এমন একটি কী তৈরি করা একটি ভাল অভ্যাস, কারণ স্ট্রিংগুলি মেলে ত্রুটি প্রবণ হতে পারে৷ মিল ঘটতে প্রতিটি কী অনন্য হতে হবে. উদাহরণস্বরূপ, জেটস্ন্যাকে আমাদের নিম্নলিখিত ভাগ করা উপাদান রয়েছে:
আপনি ভাগ করা উপাদান টাইপ প্রতিনিধিত্ব করতে একটি 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
। - নিম্নলিখিত জন্য কোন স্বয়ংক্রিয় অ্যানিমেশন সমর্থন নেই:
- শেয়ার্ড ইমেজ কম্পোজেবল :
-
ContentScale
ডিফল্টরূপে অ্যানিমেটেড নয়। এটা সেট শেষContentScale
snaps.
-
- শেপ ক্লিপিং - আকারগুলির মধ্যে স্বয়ংক্রিয় অ্যানিমেশনের জন্য কোনও অন্তর্নির্মিত সমর্থন নেই - উদাহরণস্বরূপ আইটেম রূপান্তর হিসাবে একটি বর্গ থেকে একটি বৃত্তে অ্যানিমেটিং৷
- অসমর্থিত ক্ষেত্রে,
sharedElement()
এর পরিবর্তেModifier.sharedBounds()
ব্যবহার করুন এবং আইটেমগুলিতেModifier.animateEnterExit()
যোগ করুন।
- শেয়ার্ড ইমেজ কম্পোজেবল :