در Jetpack Compose، scrollable2D و draggable2D اصلاحکنندههای سطح پایینی هستند که برای مدیریت ورودی اشارهگر در دو بعد طراحی شدهاند. درحالیکه
اصلاحکنندههای استاندارد 1D scrollable و draggable به یک جهت محدود میشوند، انواع 2D حرکت را بهطور همزمان در هر دو محور X و Y ردیابی میکنند.
برای مثال، اصلاحگر scrollable موجود برای پیمایش و پرتاب کردن با یک جهت استفاده میشود، درحالیکه scrollable2d برای پیمایش و پرتاب کردن در ۲ بُعد استفاده میشود. این امکان به شما اجازه میدهد چیدمانهای پیچیدهتری ایجاد کنید که در همه
جهتها حرکت میکنند، مثل صفحهگستردهها یا نمایشگرهای تصویر. اصلاحگر scrollable2d
همچنین از پیمایش تودرتو در سناریوهای دوبعدی پشتیبانی میکند.
scrollable2D یا draggable2D را انتخاب کنید
انتخاب درست «میانای برنامهسازی کاربردی» به عناصر «میانای کاربر» که میخواهید جابهجا کنید و رفتار فیزیکی ترجیحی برای این عناصر بستگی دارد.
Modifier.scrollable2D: از این اصلاحگر در یک محتوی برای جابهجا کردن محتوای داخل آن استفاده کنید. برای مثال، از آن با نقشهها، صفحهگستردهها، یا نمایشگرهای عکس استفاده کنید، جایی که محتوای محفظه باید در هر دو جهت افقی و عمودی پیمایش شود. این بخش شامل پشتیبانی داخلی برای پرتاب کردن است، بنابراین محتوا پساز کشیدن سریع همچنان حرکت میکند و با دیگر عناصر پیمایش در صفحه هماهنگ میشود.
Modifier.draggable2D: از این اصلاحگر برای حرکت دادن خود عنصر استفاده کنید. این یک اصلاحگر سبک است، بنابراین حرکت دقیقاً زمانی متوقف میشود که انگشت کاربر متوقف شود. شامل پشتیبانی از انتقال محتوا نمیشود.
اگر میخواهید عنصری را قابلکشیدن کنید، اما به پشتیبانی پرتاب یا پیمایش تودرتو نیاز ندارید، از draggable2D استفاده کنید.
پیادهسازی اصلاحکنندههای دوبعدی
بخشهای زیر نمونههایی از نحوه استفاده از اصلاحکنندههای دوبعدی ارائه میدهند.
پیادهسازی Modifier.scrollable2D
از این اصلاحگر برای محتویاتی استفاده کنید که کاربر باید محتوا را در همه جهتها حرکت دهد.
ضبط دادههای حرکت دوبعدی
این مثال نشان میدهد چگونه دادههای خام حرکت دوبعدی را ضبط کنید و انحراف 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، تابع لامبدایی برای مدیریت هر دلتایی که توسط انگشت کاربر تولید میشود تعریف شده است. کدoffset.value += deltaوضعیت دستی را با موقعیت جدید بهروز میکند. - عناصر
Textمقادیر X و Y فعلی آن وضعیتoffsetرا نمایش میدهند که با کشیدن کاربر بهصورت همزمان بهروزرسانی میشوند.
حرکت دادن ناحیه نمایش بزرگ
این مثال نشان میدهد چگونه از دادههای پیمایشپذیر دوبعدی ضبطشده استفاده کنید و
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محتوا را بهطور خودکار برای شما حرکت نمیدهد. درعوض، بایدoffsetردیابیشده را بااستفاده از تبدیلهایgraphicsLayerیا جبرانهای چیدمان به محتوایتان اعمال کنید. - در بلوک
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) ) } }
در گلچین قبلی:
- عنصر دوبعدی میتواند حرکت محور X را برای پیمایش داخلی مصرف کند و درعینحال، وقتی به مرزهای عمودی فرزند رسید، حرکت محور Y را به فهرست والد ارسال کند.
- بهجای اینکه کاربر را در سطح دوبعدی محدود کند، سیستم دلتای مصرفشده را محاسبه میکند و باقیمانده را به سطح بالاتر سلسلهمراتب منتقل میکند. این کار تضمین میکند که کاربر میتواند بدون برداشتن انگشتش به پیمایش در بقیه صفحه ادامه دهد.
پیادهسازی Modifier.draggable2D
برای جابهجا کردن عناصر میانای کاربر، از اصلاحگر draggable2D استفاده کنید.
کشیدن عنصر ترکیبشدنی
این مثال رایجترین مورد استفاده draggable2D را نشان میدهد — به کاربر اجازه میدهد
عنصر واسط کاربری را بردارد و آن را در هر جایی از محتوی والد قرار دهد.
@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 برای ایجاد ناحیه ورودی دوبعدی استفاده کنید
که در آن دستگیره انتخابگر در سطح خاصی محدود شده است. برخلاف مثال عنصر
کشیدنی که خود عنصر را حرکت میدهد، این پیادهسازی
از دلتاهای دوبعدی برای حرکت دادن یک عنصر ترکیبی فرزند «انتخابگر» در سراسر انتخابگر رنگ استفاده میکند:
@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را تنظیم میکند تا مطمئن شود انتخابگر هنگام کشیدن در مرکز باقی میماند.