پیمایش دوبعدی: scrollable2D،‏ draggable2D

در Jetpack Compose،‏ scrollable2D و draggable2D اصلاح‌کننده‌های سطح پایینی هستند که برای مدیریت ورودی اشاره‌گر در دو بعد طراحی شده‌اند. درحالی‌که اصلاح‌کننده‌های استاندارد 1D scrollable و draggable به یک جهت محدود می‌شوند، انواع 2D حرکت را به‌طور هم‌زمان در هر دو محور X و Y ردیابی می‌کنند.

برای مثال، اصلاح‌گر scrollable موجود برای پیمایش و پرتاب کردن با یک جهت استفاده می‌شود، درحالی‌که scrollable2d برای پیمایش و پرتاب کردن در ۲ بُعد استفاده می‌شود. این به شما امکان می‌دهد طرح‌بندی‌های پیچیده‌تری ایجاد کنید که در همه جهت‌ها حرکت می‌کنند، مانند صفحه‌گسترده‌ها یا نمایشگرهای تصویر. اصلاح‌کننده scrollable2d از پیمایش تودرتو در سناریوهای دوبعدی نیز پشتیبانی می‌کند.

شکل ۱. حرکت دوطرفه روی نقشه.

‫scrollable2D یا draggable2D را انتخاب کنید

انتخاب کردن API مناسب به عناصر میانای کاربری که می‌خواهید جابه‌جا کنید و رفتار فیزیکی ترجیحی برای این عناصر بستگی دارد.

‫Modifier.scrollable2D: از این اصلاح‌کننده در یک محفظه برای جابه‌جا کردن محتوای داخل آن استفاده کنید. برای مثال، از آن در نقشه‌ها، صفحه‌گسترده‌ها، یا نمایشگرهای عکس استفاده کنید که در آن‌ها محتوای محفظه باید در هر دو جهت افقی و عمودی پیمایش شود. این مؤلفه دارای پشتیبانی داخلی از پرتاب است تا محتوا پس از تند کشیدن همچنان حرکت کند و با سایر مؤلفه‌های پیمایش در صفحه هماهنگ شود.

Modifier.draggable2D: از این اصلاح‌کننده برای جابه‌جا کردن خود مؤلفه استفاده کنید. این یک اصلاح‌گر سبک است، بنابراین حرکت دقیقاً زمانی متوقف می‌شود که انگشت کاربر متوقف شود. این برنامه از پشتیبانی 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()}",
                    // ...
                )
            }
        }
    }
}

شکل ۲. چارگوش بنفشی که انحرافات مختصات X و Y فعلی را هنگام کشیدن اشاره‌گر روی سطح آن توسط کاربر ردیابی و نمایش می‌دهد.

تکه‌کد قبلی کارهای زیر را انجام می‌دهد:

  • از 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

این مثال نشان می‌دهد که چگونه می‌توان یک مؤلفه دوجهته را در یک والد استاندارد یک‌بعدی، مانند فید خبری عمودی، ادغام کرد.

هنگام پیاده‌سازی پیمایش تودرتو، نکات زیر را درنظر داشته باشید:

  • ‫lambda برای 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)
        )
    }
}

شکل ۵. کادر بنفش در فهرست پیمایش عمودی که امکان حرکت دوبعدی داخلی را فراهم می‌کند، اما وقتی جابه‌جایی Y داخلی کادر به حد ۳۰۰ پیکسلی خود می‌رسد، کنترل پیمایش عمودی را به فهرست اصلی منتقل می‌کند.

در گلچین قبلی:

  • مؤلفه دوبعدی می‌تواند حرکت محور X را برای حرکت افقی داخلی مصرف کند، درحالی‌که به‌طور هم‌زمان حرکت محور Y را به فهرست اصلی ارسال می‌کند، به‌محض اینکه مرزهای عمودی خود فرزند حاصل شود.
  • به‌جای اینکه کاربر را در سطح دوبعدی محدود کند، سیستم دلتای مصرف‌شده را محاسبه می‌کند و باقی‌مانده را به سطح بالاتر سلسله‌مراتب منتقل می‌کند. این کار تضمین می‌کند که کاربر می‌تواند بدون برداشتن انگشت خود، به پیمایش در بقیه صفحه ادامه دهد.

پیاده‌سازی Modifier.draggable2D

برای اطلاعات درباره نحوه استفاده از Modifier.draggable2D، به کشیدن در دو بعد با Modifier.draggable2D مراجعه کنید.