Jetpack Compose-এ, scrollable2D এবং draggable2D হলো দুটি নিম্ন-স্তরের মডিফায়ার যা দুটি মাত্রায় পয়েন্টার ইনপুট পরিচালনা করার জন্য ডিজাইন করা হয়েছে। যেখানে সাধারণ ১ডি মডিফায়ার scrollable এবং draggable একটিমাত্র অভিমুখে সীমাবদ্ধ থাকে, সেখানে এদের ২ডি সংস্করণগুলো একই সাথে X এবং Y উভয় অক্ষ বরাবর চলাচল ট্র্যাক করে।
উদাহরণস্বরূপ, বিদ্যমান scrollable মডিফায়ারটি একমুখী স্ক্রোলিং এবং ফ্লিংগিং-এর জন্য ব্যবহৃত হয়, অন্যদিকে scrollable2d টুডি (2D)-তে স্ক্রোলিং এবং ফ্লিংগিং-এর জন্য ব্যবহৃত হয়। এটি আপনাকে আরও জটিল লেআউট তৈরি করার সুযোগ দেয় যা সব দিকে চলাচল করতে পারে, যেমন স্প্রেডশিট বা ইমেজ ভিউয়ার। scrollable2d মডিফায়ারটি টুডি (2D) ক্ষেত্রে নেস্টেড স্ক্রোলিংও সমর্থন করে।
scrollable2D অথবা draggable2D বেছে নিন
সঠিক API নির্বাচন করা নির্ভর করে আপনি কোন UI উপাদানগুলো সরাতে চান এবং এই উপাদানগুলোর জন্য কাঙ্ক্ষিত ভৌত আচরণের উপর।
Modifier.scrollable2D : কোনো কন্টেইনারের ভেতরের কন্টেন্ট সরাতে এই মডিফায়ারটি ব্যবহার করুন। উদাহরণস্বরূপ, ম্যাপ, স্প্রেডশীট বা ফটো ভিউয়ারের সাথে এটি ব্যবহার করুন, যেখানে কন্টেইনারের কন্টেন্টকে আনুভূমিক এবং উল্লম্ব উভয় দিকেই স্ক্রল করার প্রয়োজন হয়। এতে বিল্ট-ইন ফ্লিং সাপোর্ট রয়েছে, ফলে সোয়াইপ করার পরেও কন্টেন্ট চলতে থাকে এবং এটি পেজের অন্যান্য স্ক্রলিং কম্পোনেন্টের সাথে সমন্বয় করে কাজ করে।
Modifier.draggable2D : কোনো কম্পোনেন্টকে সরাসরি সরাতে এই মডিফায়ারটি ব্যবহার করুন। এটি একটি লাইটওয়েট মডিফায়ার, তাই ব্যবহারকারীর আঙুল থামলেই এর নড়াচড়াও থেমে যায়। এতে ফ্লিং সাপোর্ট নেই।
যদি আপনি কোনো কম্পোনেন্টকে ড্র্যাগযোগ্য করতে চান, কিন্তু ফ্লিং বা নেস্টেড স্ক্রল সাপোর্টের প্রয়োজন না হয়, তাহলে draggable2D ব্যবহার করুন।
2D মডিফায়ার প্রয়োগ করুন
নিম্নলিখিত বিভাগগুলিতে 2D মডিফায়ারগুলি কীভাবে ব্যবহার করতে হয় তার উদাহরণ দেওয়া হয়েছে।
Modifier.scrollable2D প্রয়োগ করুন
যেসব কন্টেইনারের ভেতরের জিনিসপত্র সব দিকে সরানোর প্রয়োজন হয়, সেগুলোর জন্য এই মডিফায়ারটি ব্যবহার করুন।
দ্বিমাত্রিক গতিবিধির ডেটা সংগ্রহ করুন
এই উদাহরণটি দেখায় কিভাবে কাঁচা 2D মুভমেন্ট ডেটা ক্যাপচার করতে হয় এবং X,Y অফসেট প্রদর্শন করতে হয়:
@Composable private fun Scrollable2DSample() { // 1. Manually track the total distance the user has moved in both X and Y directions var offset by remember { mutableStateOf(Offset.Zero) } Box( modifier = Modifier .fillMaxSize() // ... contentAlignment = Alignment.Center ) { Box( modifier = Modifier .size(200.dp) // 2. Attach the 2D scroll logic to capture XY movement deltas .scrollable2D( state = rememberScrollable2DState { delta -> // 3. Update the cumulative offset state with the new movement delta offset += delta // Return the delta to indicate the entire movement was handled by this box delta } ) // ... contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { // 4. Display the current X and Y values from the offset state in real-time Text( text = "X: ${offset.x.roundToInt()}", // ... ) Spacer(modifier = Modifier.height(8.dp)) Text( text = "Y: ${offset.y.roundToInt()}", // ... ) } } } }
পূর্ববর্তী কোড স্নিপেটটি নিম্নলিখিত কাজগুলো করে:
- ব্যবহারকারীর স্ক্রল করা মোট দূরত্ব ধারণকারী একটি অবস্থা হিসেবে
offsetব্যবহার করে। -
rememberScrollable2DStateভিতরে, ব্যবহারকারীর আঙুলের দ্বারা তৈরি প্রতিটি ডেল্টা (delta) পরিচালনা করার জন্য একটি ল্যাম্বডা ফাংশন সংজ্ঞায়িত করা হয়েছে।offset.value += deltaকোডটি নতুন অবস্থান দিয়ে ম্যানুয়াল স্টেট আপডেট করে। -
Textকম্পোনেন্টগুলো সেইoffsetস্টেটের বর্তমান X এবং Y মান প্রদর্শন করে, যা ব্যবহারকারীর ড্র্যাগ করার সাথে সাথে রিয়েল-টাইমে আপডেট হয়।
একটি বড় ভিউপোর্ট প্যান করুন
এই উদাহরণটি দেখায় কিভাবে ক্যাপচার করা 2D স্ক্রোলযোগ্য ডেটা ব্যবহার করে তার প্যারেন্ট কন্টেইনারের চেয়ে বড় কন্টেন্টে translationX এবং translationY প্রয়োগ করতে হয়:
@Composable private fun Panning2DImage() { // Manually track the total distance the user has moved in both X and Y directions val offset = remember { mutableStateOf(Offset.Zero) } // Define how gestures are captured. The lambda is called for every finger movement val scrollState = rememberScrollable2DState { delta -> offset.value += delta delta } // The Viewport (Container): A fixed-size box that acts as a window into the larger content Box( modifier = Modifier .size(600.dp, 400.dp) // The visible area dimensions // ... // Hide any parts of the large content that sit outside this container's boundaries .clipToBounds() // Apply the 2D scroll modifier to intercept touch and fling gestures in all directions .scrollable2D(state = scrollState), contentAlignment = Alignment.Center, ) { // The Content: An image given a much larger size than the container viewport Image( painter = painterResource(R.drawable.cheese_5), contentDescription = null, modifier = Modifier .requiredSize(1200.dp, 800.dp) // Manual Scroll Effect: Since scrollable2D doesn't move content automatically, // we use graphicsLayer to shift the drawing position based on the tracked offset. .graphicsLayer { translationX = offset.value.x translationY = offset.value.y }, contentScale = ContentScale.FillBounds ) } }
Modifier.scrollable2D ব্যবহার করে তৈরি একটি দ্বি-মুখী প্যানিং ইমেজ ভিউপোর্ট। Modifier.scrollable2D ব্যবহার করে তৈরি একটি দ্বি-মুখী প্যানিং টেক্সট ভিউপোর্ট।পূর্ববর্তী কোড অংশে নিম্নলিখিত বিষয়গুলো অন্তর্ভুক্ত রয়েছে:
- কন্টেইনারটিকে একটি নির্দিষ্ট আকারে (
600x400dp) সেট করা হয়েছে, অন্যদিকে কন্টেন্টকে অনেক বড় আকার (1200x800dp) দেওয়া হয়েছে, যাতে এটি তার প্যারেন্টের আকারে রিসাইজ না হয়। - কন্টেইনারের
clipToBounds()মডিফায়ারটি নিশ্চিত করে যে, বড় কন্টেন্টের যে কোনো অংশ যা600x400বক্সের বাইরে থাকে, তা দৃষ্টির আড়ালে থাকবে। -
LazyColumnমতো উচ্চ-স্তরের কম্পোনেন্টগুলোর বিপরীতে,scrollable2Dস্বয়ংক্রিয়ভাবে আপনার জন্য কন্টেন্ট সরিয়ে দেয় না। এর পরিবর্তে, আপনাকেgraphicsLayerট্রান্সফরমেশন অথবা লেআউট অফসেট ব্যবহার করে আপনার কন্টেন্টে ট্র্যাক করাoffsetপ্রয়োগ করতে হবে। -
graphicsLayerব্লকের ভিতরে,translationX = offset.value.xএবংtranslationY = offset.value.yআপনার আঙুলের নড়াচড়ার উপর ভিত্তি করে ছবি বা লেখার অঙ্কন অবস্থান পরিবর্তন করে, যা স্ক্রলিংয়ের ভিজ্যুয়াল ইফেক্ট তৈরি করে।
scrollable2D ব্যবহার করে নেস্টেড স্ক্রলিং বাস্তবায়ন করুন
এই উদাহরণটি দেখায় কিভাবে একটি দ্বি-মুখী কম্পোনেন্টকে একটি সাধারণ এক-মাত্রিক প্যারেন্টের সাথে, যেমন একটি ভার্টিকাল নিউজ ফিড, ইন্টিগ্রেট করা যায়।
নেস্টেড স্ক্রলিং প্রয়োগ করার সময় নিম্নলিখিত বিষয়গুলো মনে রাখবেন:
-
rememberScrollable2DStateএর ল্যাম্বডা শুধুমাত্র ব্যবহৃত ডেল্টা ফেরত দেবে, যাতে চাইল্ড লিস্ট তার সীমায় পৌঁছালে প্যারেন্ট লিস্ট স্বাভাবিকভাবে দায়িত্ব নিতে পারে । - যখন কোনো ব্যবহারকারী একটি ডায়াগোনাল ফ্লিং সম্পাদন করে, তখন দ্বি-মাত্রিক বেগটি শেয়ার করা হয়। অ্যানিমেশন চলাকালীন যদি চাইল্ডটি কোনো সীমানায় আঘাত করে, তবে স্ক্রলটি স্বাভাবিকভাবে চালিয়ে যাওয়ার জন্য অবশিষ্ট ভরবেগ প্যারেন্টের কাছে স্থানান্তরিত হয় ।
@Composable private fun NestedScrollable2DSample() { var offset by remember { mutableStateOf(Offset.Zero) } val maxScrollDp = 250.dp val maxScrollPx = with(LocalDensity.current) { maxScrollDp.toPx() } Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .background(Color(0xFFF5F5F5)), horizontalAlignment = Alignment.CenterHorizontally ) { Text( "Scroll down to find the 2D Box", modifier = Modifier.padding(top = 100.dp, bottom = 500.dp), style = TextStyle(fontSize = 18.sp, color = Color.Gray) ) // The Child: A 2D scrollable box with nested scroll coordination Box( modifier = Modifier .size(250.dp) .scrollable2D( state = rememberScrollable2DState { delta -> val oldOffset = offset // Calculate new potential offset and clamp it to our boundaries val newX = (oldOffset.x + delta.x).coerceIn(-maxScrollPx, maxScrollPx) val newY = (oldOffset.y + delta.y).coerceIn(-maxScrollPx, maxScrollPx) val newOffset = Offset(newX, newY) // Calculate exactly how much was consumed by the child val consumed = newOffset - oldOffset offset = newOffset // IMPORTANT: Return ONLY the consumed delta. // The remaining (unconsumed) delta propagates to the parent Column. consumed } ) // ... contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { val density = LocalDensity.current Text("2D Panning Zone", color = Color.White.copy(alpha = 0.7f), fontSize = 12.sp) Spacer(Modifier.height(8.dp)) Text("X: ${with(density) { offset.x.toDp().value.roundToInt() }}dp", color = Color.White, fontWeight = FontWeight.Bold) Text("Y: ${with(density) { offset.y.toDp().value.roundToInt() }}dp", color = Color.White, fontWeight = FontWeight.Bold) } } Text( "Once the Purple Box hits Y: 250 or -250,\nthis parent list will take over the vertical scroll.", textAlign = TextAlign.Center, modifier = Modifier.padding(top = 40.dp, bottom = 800.dp), style = TextStyle(fontSize = 14.sp, color = Color.Gray) ) } }
পূর্ববর্তী কোড অংশে:
- 2D কম্পোনেন্টটি অভ্যন্তরীণভাবে প্যান করার জন্য X-অক্ষের মুভমেন্ট গ্রহণ করতে পারে এবং একই সাথে, চাইল্ড কম্পোনেন্টের নিজস্ব উল্লম্ব সীমানায় পৌঁছানোর পর Y-অক্ষের মুভমেন্ট প্যারেন্ট লিস্টে প্রেরণ করতে পারে।
- ব্যবহারকারীকে দ্বিমাত্রিক তলের মধ্যে আটকে রাখার পরিবর্তে, সিস্টেমটি ব্যবহৃত ডেল্টা গণনা করে এবং অবশিষ্ট অংশটি স্তরক্রমের উপরের দিকে পাঠিয়ে দেয়। এটি নিশ্চিত করে যে ব্যবহারকারী আঙুল না তুলেই পৃষ্ঠার বাকি অংশ স্ক্রল করে যেতে পারেন।
Modifier.draggable2D প্রয়োগ করুন
স্বতন্ত্র UI উপাদানগুলো সরানোর জন্য draggable2D মডিফায়ারটি ব্যবহার করুন।
একটি রচনাযোগ্য উপাদান টেনে আনুন
এই উদাহরণটি draggable2D এর সবচেয়ে সাধারণ ব্যবহার দেখায় — যা একজন ব্যবহারকারীকে একটি UI এলিমেন্ট তুলে নিয়ে তার প্যারেন্ট কন্টেইনারের মধ্যে যেকোনো জায়গায় পুনরায় স্থাপন করার সুযোগ দেয়।
@Composable private fun DraggableComposableElement() { // 1. Track the position of the floating window var offset by remember { mutableStateOf(Offset.Zero) } Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) { Box( modifier = Modifier // 2. Apply the offset to the box's position .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } // ... // 3. Attach the 2D drag logic .draggable2D( state = rememberDraggable2DState { delta -> // 4. Update the position based on the movement delta offset += delta } ), contentAlignment = Alignment.Center ) { Text("Video Preview", color = Color.White, fontSize = 12.sp) } } }
পূর্ববর্তী কোড স্নিপেটটিতে নিম্নলিখিত বিষয়গুলো অন্তর্ভুক্ত রয়েছে:
- একটি
offsetঅবস্থা ব্যবহার করে বক্সটির অবস্থান ট্র্যাক করে। - ড্র্যাগ ডেল্টার উপর ভিত্তি করে কম্পোনেন্টের অবস্থান পরিবর্তন করতে
offsetমডিফায়ার ব্যবহার করা হয়। - যেহেতু এতে কোনো নিক্ষেপ সমর্থন নেই, তাই ব্যবহারকারী আঙুল তুলে নেওয়ার সাথে সাথেই বাক্সটি নড়াচড়া বন্ধ করে দেয়।
প্যারেন্টের ড্র্যাগ এরিয়ার উপর ভিত্তি করে একটি চাইল্ড কম্পোজেবল ড্র্যাগ করুন।
এই উদাহরণটি দেখায় কিভাবে draggable2D ব্যবহার করে একটি 2D ইনপুট এরিয়া তৈরি করা যায়, যেখানে একটি সিলেক্টর নব একটি নির্দিষ্ট পৃষ্ঠতলের মধ্যে সীমাবদ্ধ থাকে। draggable এলিমেন্ট উদাহরণের মতো নয়, যা কম্পোনেন্টটিকেই সরিয়ে দেয়, এই বাস্তবায়নটি 2D ডেল্টা ব্যবহার করে একটি চাইল্ড কম্পোজেবল 'সিলেক্টর'-কে একটি কালার পিকারের উপর দিয়ে সরিয়ে নিয়ে যায়:
@Composable private fun ExampleColorSelector( // ... ) { // 1. Maintain the 2D position of the selector in state. var selectorOffset by remember { mutableStateOf(Offset.Zero) } // 2. Track the size of the background container. var containerSize by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .size(300.dp, 200.dp) // Capture the actual pixel dimensions of the container when it's laid out. .onSizeChanged { containerSize = it } .clip(RoundedCornerShape(12.dp)) .background( brush = remember(hue) { // Create a simple gradient representing Saturation and Value for the given Hue. Brush.linearGradient(listOf(Color.White, Color.hsv(hue, 1f, 1f))) } ) ) { Box( modifier = Modifier .size(24.dp) .graphicsLayer { // Center the selector on the finger by subtracting half its size. translationX = selectorOffset.x - (24.dp.toPx() / 2) translationY = selectorOffset.y - (24.dp.toPx() / 2) } // ... // 3. Configure 2D touch dragging. .draggable2D( state = rememberDraggable2DState { delta -> // 4. Calculate the new position and clamp it to the container bounds val newX = (selectorOffset.x + delta.x) .coerceIn(0f, containerSize.width.toFloat()) val newY = (selectorOffset.y + delta.y) .coerceIn(0f, containerSize.height.toFloat()) selectorOffset = Offset(newX, newY) } ) ) } }
পূর্ববর্তী কোড অংশে নিম্নলিখিত বিষয়গুলো অন্তর্ভুক্ত রয়েছে:
- এটি গ্রেডিয়েন্ট কন্টেইনারের প্রকৃত মাত্রা ক্যাপচার করতে
onSizeChangedমডিফায়ার ব্যবহার করে। সিলেক্টরটি জানে এজগুলো ঠিক কোথায় অবস্থিত। -
graphicsLayerভিতরে, ড্র্যাগ করার সময় সিলেক্টরটি যাতে কেন্দ্রে থাকে তা নিশ্চিত করার জন্যtranslationXএবংtranslationYসামঞ্জস্য করা হয়।