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

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

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

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

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

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

‫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 مراجعه کنید.