در Jetpack Compose، scrollable2D و draggable2D اصلاحکنندههای سطح پایینی هستند که برای مدیریت ورودی اشارهگر در دو بعد طراحی شدهاند. در حالی که اصلاحکنندههای استاندارد یکبعدی، scrollable و draggable به یک جهتگیری محدود هستند، انواع دوبعدی حرکت را در هر دو محور X و Y به طور همزمان ردیابی میکنند.
برای مثال، اصلاحکنندهی scrollable موجود برای اسکرول کردن و جابجایی در یک جهت استفاده میشود، در حالی که scrollable2d برای اسکرول کردن و جابجایی در حالت دوبعدی استفاده میشود. این به شما امکان میدهد طرحبندیهای پیچیدهتری ایجاد کنید که در همه جهات حرکت میکنند، مانند صفحات گسترده یا نمایشگرهای تصویر. اصلاحکنندهی scrollable2d همچنین از پیمایش تو در تو در سناریوهای دوبعدی پشتیبانی میکند.
انتخاب گزینه scrollable2D یا draggable2D
انتخاب API مناسب به عناصر رابط کاربری که میخواهید جابجا کنید و رفتار فیزیکی ترجیحی برای این عناصر بستگی دارد.
Modifier.scrollable2D : از این اصلاحکننده روی یک ظرف برای جابجایی محتوا درون آن استفاده کنید. برای مثال، از آن با نقشهها، صفحات گسترده یا نمایشگرهای عکس استفاده کنید، جایی که محتوای ظرف نیاز به پیمایش در هر دو جهت افقی و عمودی دارد. این شامل پشتیبانی داخلی از fling است تا محتوا پس از کشیدن انگشت به حرکت خود ادامه دهد و با سایر اجزای پیمایش در صفحه هماهنگ شود.
Modifier.draggable2D : از این مدیفایر برای حرکت دادن خودِ یک کامپوننت استفاده کنید. این یک مدیفایر سبک است، بنابراین حرکت دقیقاً زمانی که انگشت کاربر متوقف میشود، متوقف میشود. این مدیفایر شامل پشتیبانی از حرکت سریع (fling) نمیشود.
اگر میخواهید یک کامپوننت را قابل کشیدن کنید، اما به پشتیبانی از fling یا پیمایش تو در تو نیاز ندارید، 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 .قطعه کد قبلی شامل موارد زیر است:
- اندازه کانتینر (container) روی یک اندازه ثابت (
600x400dp) تنظیم شده است، در حالی که اندازه محتوا (content) بسیار بزرگتر (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) } } }
قطعه کد قبلی شامل موارد زیر است:
- موقعیت جعبه را با استفاده از یک حالت
offsetstate) ردیابی میکند. - از اصلاحکنندهی
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را تنظیم میکند تا مطمئن شود که انتخابگر هنگام کشیدن در مرکز باقی میماند.