İki boyutlu kaydırma: scrollable2D, draggable2D

Jetpack Compose'da scrollable2D ve draggable2D, işaretçi girişini iki boyutta işlemek için tasarlanmış alt düzey değiştiricilerdir. Standart 1D değiştiriciler scrollable ve draggable tek bir yönle sınırlıyken 2D varyantlar hem X hem de Y eksenindeki hareketi aynı anda izler.

Örneğin, mevcut scrollable değiştiricisi tek yönlü kaydırma ve kaydırma için kullanılırken scrollable2d, 2D'de kaydırma ve kaydırma için kullanılır. Bu sayede, e-tablolar veya resim görüntüleyiciler gibi her yöne hareket eden daha karmaşık düzenler oluşturabilirsiniz. scrollable2d değiştirici, 2D senaryolarda iç içe yerleştirilmiş kaydırmayı da destekler.

Şekil 1. Haritada iki yönlü kaydırma.

scrollable2D veya draggable2D seçeneğini belirleyin.

Doğru API'yi seçmek, taşımak istediğiniz kullanıcı arayüzü öğelerine ve bu öğeler için tercih edilen fiziksel davranışa bağlıdır.

Modifier.scrollable2D: İçeriği taşımak için bu değiştiriciyi bir kapsayıcıda kullanın. Örneğin, kapsayıcı içeriğinin hem yatay hem de dikey yönde kaydırılması gereken haritalar, e-tablolar veya fotoğraf görüntüleyicilerle kullanın. Yerleşik kaydırma desteği sayesinde, kaydırma işleminden sonra içerik hareket etmeye devam eder ve sayfadaki diğer kaydırma bileşenleriyle koordineli çalışır.

Modifier.draggable2D: Bir bileşeni taşımak için bu değiştiriciyi kullanın. Hafif bir değiştirici olduğundan, kullanıcının parmağı durduğunda hareket de durur. Fling desteği yoktur.

Bir bileşeni sürüklenebilir yapmak istiyorsanız ancak kaydırma veya iç içe yerleştirilmiş kaydırma desteğine ihtiyacınız yoksa draggable2D kullanın.

2D değiştiricileri uygulama

Aşağıdaki bölümlerde, 2D değiştiricilerin nasıl kullanılacağını gösteren örnekler verilmiştir.

Modifier.scrollable2D uygulayın

Kullanıcının içeriği her yöne taşıması gereken kapsayıcılar için bu değiştiriciyi kullanın.

2D hareket verilerini yakalama

Bu örnekte, ham 2D hareket verilerinin nasıl yakalanacağı ve X,Y ofsetinin nasıl gösterileceği açıklanmaktadır:

@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()}",
                    // ...
                )
            }
        }
    }
}

Şekil 2. Kullanıcı işaretçiyi yüzeyinde sürüklerken mevcut X ve Y koordinat farklarını izleyen ve gösteren mor bir kutu.

Yukarıdaki snippet şu işlemleri yapar:

  • Kullanıcının kaydırdığı toplam mesafeyi tutan bir durum olarak offset kullanır.
  • rememberScrollable2DState içinde, kullanıcının parmağıyla oluşturulan her deltayı işlemek için bir lambda işlevi tanımlanır. offset.value += delta kodu, manuel durumu yeni konumla günceller.
  • Text bileşenleri, ilgili offset durumunun mevcut X ve Y değerlerini gösterir. Bu değerler, kullanıcı sürükledikçe gerçek zamanlı olarak güncellenir.

Büyük bir görüntü alanını kaydırma

Bu örnekte, yakalanan 2D kaydırılabilir verilerin nasıl kullanılacağı ve üst kapsayıcısından daha büyük olan içeriğe translationX ile translationY öğelerinin nasıl uygulanacağı gösterilmektedir:

@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
        )
    }
}

3. şekil Modifier.scrollable2D ile oluşturulan, çift yönlü kaydırma özellikli resim görüntüleme alanı.
Şekil 4. Modifier.scrollable2D ile oluşturulan, iki yönlü kaydırılabilir metin görüntüleme alanı.

Önceki snippet aşağıdakileri içerir:

  • Kapsayıcı sabit bir boyuta (600x400dp) ayarlanırken içeriğe, üst boyutuna göre yeniden boyutlandırılmasını önlemek için çok daha büyük bir boyut (1200x800dp) verilir.
  • Kapsayıcıdaki clipToBounds() değiştirici, büyük içeriğin 600x400 kutusunun dışında kalan kısımlarının görünümden gizlenmesini sağlar.
  • LazyColumn gibi üst düzey bileşenlerin aksine scrollable2D, içeriği sizin için otomatik olarak taşımaz. Bunun yerine, graphicsLayer dönüşümlerini veya düzen ofsetlerini kullanarak izlenen offset öğesini içeriğinize uygulamanız gerekir.
  • graphicsLayer bloğunun içinde, translationX = offset.value.x ve translationY = offset.value.y, parmağınızın hareketine göre resmin veya metnin çizim konumunu değiştirerek kaydırma görsel efektini oluşturur.

scrollable2D ile iç içe yerleştirilmiş kaydırmayı uygulama

Bu örnekte, iki yönlü bir bileşenin dikey haber feed'i gibi standart tek boyutlu bir üst öğeye nasıl entegre edilebileceği gösterilmektedir.

İç içe yerleştirilmiş kaydırma özelliğini uygularken aşağıdaki noktaları göz önünde bulundurun:

  • rememberScrollable2DState için lambda, yalnızca tüketilen deltayı döndürmelidir. Böylece alt liste sınırına ulaştığında üst liste doğal olarak devralabilir.
  • Kullanıcı çapraz kaydırma yaptığında 2D hız paylaşılır. Alt öğe, animasyon sırasında bir sınıra ulaşırsa kalan momentum, kaydırma işleminin doğal bir şekilde devam etmesi için üst öğeye yayılır.

@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)
        )
    }
}

Şekil 5. Dikey kaydırma listesinde yer alan mor bir kutu, dahili 2D harekete izin verir ancak kutunun dahili Y ekseni kayması 300 piksellik sınırına ulaştığında dikey kaydırma kontrolünü üst listeye aktarır.

Yukarıdaki snippet'te:

  • 2D bileşen, dahili olarak kaydırmak için X ekseni hareketini kullanabilir ve aynı zamanda çocuğun kendi dikey sınırlarına ulaşıldığında Y ekseni hareketini üst listeye gönderebilir.
  • Sistem, kullanıcıyı 2D yüzeyde tutmak yerine tüketilen farkı hesaplar ve kalanı hiyerarşide yukarı iletir. Bu sayede kullanıcı, parmağını kaldırmadan sayfanın geri kalanında gezinmeye devam edebilir.

Modifier.draggable2D uygulayın

Modifier.draggable2D özelliğinin nasıl kullanılacağı hakkında bilgi için Modifier.draggable2D ile iki boyutta sürükleme başlıklı makaleyi inceleyin.