Birleştirilebilir öğeleri sürükleme

Kullanıcıların bileşenleri ekranda fiziksel olarak sürüklemesine olanak tanımak, modern Android uygulamalarının temel özelliklerinden biridir. Jetpack Compose, özel bir kaydırıcı, kaydırarak kapatma bileşeni veya etkileşimli bir tuval oluştururken sürükleme etkinliklerini kolayca yönetmek için kapsamlı bir API seti sunar.

Compose sürükleme API'leri, ham dokunma etkinliklerinin karmaşıklığını ortadan kaldırır. pointerInput ile dokunma koordinatlarını manuel olarak hesaplamak ve hareket yaşam döngülerini yönetmek yerine, doğrudan Compose'un durum yönetimi ve animasyon sistemlerine bağlanan yerleşik değiştiricileri kullanabilirsiniz.

Bu sayfada, her bir sürükleme API'sinin nasıl ve ne zaman kullanılacağı açıklanmaktadır.

Doğru sürükleme API'sini seçme

Sürüklenebilir içerik uygulamak için kullanabileceğiniz çeşitli API'ler vardır.

  • Şu durumlarda standart Material bileşenlerini (ör. SwipeToDismissBox veya gezinme çekmeceleri) seçin: Varsayılan Material 3 kaydırma veya sürükleme davranışına ihtiyacınız olduğunda (ör. e-posta silme, görev arşivleme veya yan menü / alt sayfaları açma). Özel durum veya düzen yönetimi gerektirmeden yerleşik davranışlar sunarlar.
  • Şu durumlarda Modifier.draggable seçin: Yalnızca ham sürükleme deltalarına ihtiyacınız olduğunda (örneğin, özel bir kaydırma çubuğunu sürükleme). Daha fazla bilgi için Sürüklenebilir ile tek bir eksen boyunca sürükleme başlıklı makaleyi inceleyin.
    • Şu durumlarda Modifier.draggable2D seçin: Bir bileşenin hem X hem de Y eksenleri boyunca aynı anda sürekli hareket etmesi gerektiğinde (örneğin, bir harita üzerinde kaydırma yaparken veya kullanıcının ekranda herhangi bir yere sürükleyebileceği kayan işlem düğmesi oluştururken). Daha fazla bilgi için Modifier.draggable2D ile iki boyutta sürükleme başlıklı makaleyi inceleyin.
  • Şu durumlarda Modifier.anchoredDraggable seçin: İki veya daha fazla ayrı konum arasında geçiş yapması gereken özel etkileşimli bileşenler (ör. alt sayfa, gezinme çekmecesi veya kaydırarak gösterme işlemi) oluşturuyorsanız. Daha fazla bilgi için anchoredDraggable ile bileşenleri bağlantı noktaları arasında sürükleme başlıklı makaleyi inceleyin.

draggable ile tek bir eksen boyunca sürükleyin

draggable değiştiricisi, tek bir yöndeki sürükleme hareketleri için üst düzey giriş noktasıdır ve sürükleme mesafesini piksel cinsinden bildirir.

Bu değiştiricinin, yalnızca hareketi algılaması açısından scrollable ile benzer olduğunu belirtmek isteriz. Durumu tutmanız ve ekranda göstermeniz gerekir. Örneğin, öğeyi offset değiştiricisiyle taşıyabilirsiniz:

@Composable
private fun DraggableText() {
    var offsetX by remember { mutableFloatStateOf(0f) }
    Text(
        modifier = Modifier
            .offset { IntOffset(offsetX.roundToInt(), 0) }
            .draggable(
                orientation = Orientation.Horizontal,
                state = rememberDraggableState { delta ->
                    offsetX += delta
                }
            ),
        text = "Drag me!"
    )
}

Sürükleme hareketinin tamamını kontrol etmeniz gerekiyorsa pointerInput değiştiricisi aracılığıyla sürükleme hareketi algılayıcısını kullanabilirsiniz.

@Composable
private fun DraggableTextLowLevel() {
    Box(modifier = Modifier.fillMaxSize()) {
        var offsetX by remember { mutableFloatStateOf(0f) }
        var offsetY by remember { mutableFloatStateOf(0f) }

        Box(
            Modifier
                .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
                .background(Color.Blue)
                .size(50.dp)
                .pointerInput(Unit) {
                    detectDragGestures { change, dragAmount ->
                        change.consume()
                        offsetX += dragAmount.x
                        offsetY += dragAmount.y
                    }
                }
        )
    }
}

INSERT ALT TEXT HERE
Şekil 1. Bir parmak tarafından sürüklenen kullanıcı arayüzü öğesi

anchoredDraggable ile bileşenleri bağlantı noktaları arasında sürükleme

anchoredDraggable değiştiricisi, bir bileşeni bağlantı noktaları adı verilen önceden tanımlanmış bir dizi konum arasında sürüklemenize olanak tanır. Kullanıcı sürükleme hareketini bıraktığında bileşen en yakın sabitleyiciye yerleşir. anchoredDraggable, özel alt sayfalar, karmaşık kaydırma çubukları veya çekmece panelleri gibi bileşenler oluşturmak için kullanışlıdır.

API yüzeyi

AnchoredDraggable API'si aşağıdaki ana bölümlerden oluşur:

  • Anchors (DraggableAnchors): Özel durumlarınız (ör. Start, Center, End için bir enum sınıfı) ile bunlara karşılık gelen piksel ofset değerleri arasındaki eşlemeyi tanımlayan bir oluşturucudur.
  • State (AnchoredDraggableState<T>): Bileşenin mevcut konumunu (ofset), yerleştiği sabitleme noktasını ve sürükleme ilerleme durumunu yöneten bir durum bilgisi depolayıcıdır. Yapılandırma değişikliklerinde durumunu korumak için rememberSaveable ve AnchoredDraggableState.Saver<T> kullanılarak oluşturulmalıdır.
  • Hareket değiştirici (Modifier.anchoredDraggable): Orientation boyunca sürükleme hareketlerini algılayan, durumu sürükleme deltasıyla güncelleyen ve bileşeni serbest bırakıldığında en yakın sabitleyiciye animasyonla taşımak için kaydırma hareketini işleyen bir değiştirici.

Örnek: Özel bir SwipeToDismiss bileşeni oluşturma

Bu örnekte, kullanıcının sola kaydırarak kapatabileceği veya sağa sürükleyerek aşırı kaydırma esnetme efektini görebileceği özel bir SwipeToDismiss bileşeninin (beyaz banner) nasıl oluşturulacağı gösterilmektedir.

  1. Sabit değerleri tanımlayın. Sürüklenebilir içeriğin yerleşebileceği ayrı konumları temsil eden bir enum tanımlayın:

    private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }

  2. AnchoredDraggableState örneğini oluşturun ve kaydedin. Bileşenin mevcut konumunu etkinlik yeniden oluşturma ve yapılandırma değişiklikleri (ör. ekran döndürme veya tema değişiklikleri) boyunca korumak için AnchoredDraggableState.Saver() ile rememberSaveable kullanın:

    val state =
        rememberSaveable(saver = AnchoredDraggableState.Saver()) {
            AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting)
        }

  3. layout, offset ve draggable değiştiricilerini uygulayın. Bileşeni sürüklenebilir hale getirmek ve bağlantı noktalarının boyutuna uyum sağlamasını sağlamak için görsel olarak hareket etmesi gereken içeriğe aşağıdaki değiştiricileri uygulayın:

    • Modifier.layout: Sabit konumlar genellikle bir bileşenin boyutuna bağlıdır. Bu boyut, ilk oluşturma sırasında bilinmez. Bileşeni ölçmek için layout değiştiricisini kullanın, DraggableAnchors haritasını genişliğine göre dinamik olarak hesaplayın ve state.updateAnchors() kullanarak durumu güncelleyin.

    • Modifier.anchoredDraggable: Duruma bağlanır, belirtilen yönde sürükleme deltalarını gönderir ve bileşeni serbest bırakıldığında en yakın sabitleyiciye animasyonla taşımak için kaydırma hareketini işler. Bu değiştiriciyi, sürükleme hareketlerine yanıt vermesi gereken bileşene ekleyin ve state ile sürükleme orientation (ve isteğe bağlı olarak overscrollEffect) değerlerini iletin.
    • Modifier.offset: Ofset lambda içindeki state.requireOffset() öğesinden ofseti okur. Ofsetin bu lambda içinde okunması, değerin yalnızca yerleşim aşamasında okunmasını sağlayarak sürükleme sırasında gereksiz yeniden oluşturma işlemlerini önler. Bu değiştiriciyi, görsel olarak hareket etmesi gereken içeriğe uygulayın.

    Box(
        modifier = Modifier
            .fillMaxWidth()
            // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase
            .offset { IntOffset(state.requireOffset().roundToInt(), 0) }
            .layout { measurable, constraints ->
                val placeable = measurable.measure(constraints)
                val anchors = DraggableAnchors {
                    AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat()
                    AnchoredDraggableSwipeDismissValue.Resting at 0f
                }
                state.updateAnchors(anchors)
                layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
            }
            // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll.
            .overscroll(overscrollEffect)
            .anchoredDraggable(
                state,
                Orientation.Horizontal,
                overscrollEffect = overscrollEffect
            )
    ) {
        dismissContent()
    }

  4. [İsteğe bağlı] Kaydırma sonunu entegre edin. overscrollEffect API'si, kaydırılabilir bir kapsayıcının kenarlarına kaydırma veya hızlıca kaydırma hareketiyle ulaşıldığında gösterilen bir görsel efekt uygular. Kullanıcı, tanımlanan sabit sınırların ötesine sürüklediğinde kaydırma sonu efektiyle (genellikle esneme veya parlama) görsel geri bildirim sağlayabilirsiniz.

    1. Gönderme: anchoredDraggable değiştiricisine bir overscrollEffect örneği iletin. Bu, hareketin sabitleme sınırlarının içinde veya dışında olmasına bağlı olarak sürükleme deltalarını AnchoredDraggable durumuna ya da kaydırma sonu efektine gönderir.
    2. Oluşturma: Aynı OverscrollEffect örneğine sahip Modifier.overscroll'ı, efekti görsel olarak oluşturması gereken bileşene uygulayın.

Aşağıdaki snippet'te standart bir esnetme efektinin nasıl ekleneceği gösterilmektedir:

val overscrollEffect = rememberOverscrollEffect()
Box(
    modifier.fillMaxWidth()
) {
    Box(Modifier.fillMaxWidth()) {
        background()
    }
    Box(
        modifier = Modifier
            .fillMaxWidth()
            // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase
            .offset { IntOffset(state.requireOffset().roundToInt(), 0) }
            .layout { measurable, constraints ->
                val placeable = measurable.measure(constraints)
                val anchors = DraggableAnchors {
                    AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat()
                    AnchoredDraggableSwipeDismissValue.Resting at 0f
                }
                state.updateAnchors(anchors)
                layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
            }
            // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll.
            .overscroll(overscrollEffect)
            .anchoredDraggable(
                state,
                Orientation.Horizontal,
                overscrollEffect = overscrollEffect
            )
    ) {
        dismissContent()
    }
}

Sonuç

Yukarıdaki kod snippet'i, "Kapatmak için beni sürükleyin!" metnini içeren beyaz bir banner oluşturur. Kullanıcı, bu banner'ı sola kaydırarak kapatabilir (veya sağa sürükleyerek aşırı kaydırma esnetme efektini görebilir):

Video 2. Özel bir kaydırarak kapatma bileşeni uygulaması.

Örnek: Özel, hareketle çalışan gezinme çekmecesi oluşturma

Bu örnekte, özel ve hareketle çalışan bir gezinme çekmecesinin nasıl oluşturulacağı gösterilmektedir. Kullanıcı yatay olarak sürükledikçe ön plan ekranı (ScreenContents) sağa kayar, 1.0f olan ölçeği 0.8f değerine düşer ve köşeleri yuvarlanarak alttaki gezinme çekmecesini gösterir.

  1. Sabit durumları ve konumları tanımlayın. Bu örnekte, bağlantı noktaları çekmecenin açık ve kapalı durumlarıdır. Open ve Closed durumları için bir enum tanımlayın ve DraggableAnchors kullanarak bunları piksel ofsetleriyle eşleyin:

    enum class DrawerState {
        Open,
        Closed
    }
    
        val anchors = DraggableAnchors {
            DrawerState.Open at drawerWidth
            DrawerState.Closed at 0f
        }
        val state = remember {
            AnchoredDraggableState(
                initialValue = DrawerState.Closed,
                anchors = anchors,
            )
        }

  2. Bir öğe tıklandığında anchoredDraggableState animasyonunu oynatın. HomeScreenDrawer Box'in altına, ScreenContents ise üstüne yerleştirilir. Çekmece sabit kalırken üst katman, çekmecenin üzerinde hareket eder ve ölçeklenir.

    Kullanıcı bir çekmece öğesi seçtiğinde etkin ekran durumunu güncelleyin ve state.animateTo(DrawerState.Closed) kullanarak çekmeceyi otomatik olarak kapatın:

    @Composable
    private fun AnchoredDraggableDrawerOnClickSample(
        modifier: Modifier = Modifier,
        state: AnchoredDraggableState<DrawerState>,
        coroutineScope: CoroutineScope
    ) {
        var screenState by remember { mutableStateOf(ScreenState.Home) }
    
        Box(modifier = modifier.fillMaxSize()) {
            HomeScreenDrawer(
                selectedScreen = screenState,
                onScreenSelected = { screen ->
                    screenState = screen
                    // Dismiss drawer when a destination is selected
                    coroutineScope.launch { state.animateTo(DrawerState.Closed) }
                }
            )
    
            // ScreenContents(...)
        }
    }

  3. Birden fazla görsel özelliği değiştirmek için state.requireOffset() kullanın. Yalnızca içeriği kaydırmak yerine, Modifier.graphicsLayer ile birden fazla özelliği değiştirerek sürükleme ilerlemesine göre çeviri, ölçek ve köşe yarıçapını aynı anda canlandırabilirsiniz:

    • Mevcut sürükleme ofsetini almak için state.requireOffset() kullanın.
    • Ofset ve çekmece genişliğine göre orantılı ölçeği (1f - 0.8f) ve köşe yarıçapını (0.dp - 32.dp) hesaplamak için lerp() (doğrusal interpolasyon) kullanın.
    • Modifier.graphicsLayer, değişiklikleri çizim aşamasında uygulayarak sürükleme sırasında yeniden oluşturma işlemlerini tetiklemeden yüksek performanslı animasyonlar sağlar.

    @Composable
    private fun AnchoredDraggableDrawerGraphicsLayerSample(
        state: AnchoredDraggableState<DrawerState>,
        drawerWidth: Float,
        selectedScreen: ScreenState,
        onDrawerClicked: () -> Unit
    ) {
        ScreenContents(
            selectedScreen = selectedScreen,
            onDrawerClicked = onDrawerClicked,
            modifier = Modifier
                .graphicsLayer {
                    this.translationX = state.requireOffset()
                    val scale = lerp(1f, 0.8f, state.requireOffset() / drawerWidth)
                    this.scaleX = scale
                    this.scaleY = scale
                    val cornerRadius = lerp(0f, 32f, state.requireOffset() / drawerWidth)
                    this.shape = RoundedCornerShape(cornerRadius.dp)
                    this.clip = true
                }
                .anchoredDraggable(state, Orientation.Horizontal)
        )
    }

  4. Programatik animasyonları animateTo ile tetikleyin. Çekmeceyi programatik olarak kontrol etmek için (ör. menü simgesine dokunurken veya bir çekmece öğesi seçerken) bir eş yordam kapsamında state.animateTo() işlevini başlatın:

    fun toggleDrawerState(
        state: AnchoredDraggableState<DrawerState>,
        coroutineScope: CoroutineScope
    ) {
        coroutineScope.launch {
            if (state.currentValue == DrawerState.Open) {
                state.animateTo(DrawerState.Closed)
            } else {
                state.animateTo(DrawerState.Open)
            }
        }
    }

Sonuç

Video 3. Ön plandaki ekran (Uyku) sağa sürüklenerek altındaki gezinme çekmecesi gösteriliyor.

Modifier.draggable2D ile iki boyutta sürükleme

Kullanıcı arayüzü öğelerini hem X hem de Y ekseninde aynı anda taşımak için draggable2D değiştiricisini kullanın.

Birleştirilebilir öğeyi sürükleme

Bu örnekte, draggable2D için en yaygın kullanım alanı gösterilmektedir: Kullanıcının bir kullanıcı arayüzü öğesini seçip üst kapsayıcıda herhangi bir yere yeniden konumlandırmasına olanak tanıma.

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

Video 4. Gri arka plan üzerinde yeniden konumlandırılan küçük mor kutu, kullanıcının parmağı kaldırıldığı anda öğenin hareket etmeyi bıraktığı doğrudan 2D sürüklemeyi gösteriyor.

Yukarıdaki kod snippet'i şunları içerir:

  • offset durumunu kullanarak kutunun konumunu izler.
  • Sürükleme deltalarına göre bileşenin konumunu değiştirmek için offset değiştiricisini kullanır.
  • Kaydırma desteği olmadığından, kullanıcı parmağını kaldırdığı anda kutu hareket etmeyi durdurur.

Üst öğenin sürükleme alanına göre alt composable'ı sürükleme

Bu örnekte, seçici düğmesinin belirli bir yüzeyle sınırlandırıldığı 2D giriş alanı oluşturmak için draggable2D özelliğinin nasıl kullanılacağı gösterilmektedir. Bileşenin kendisini taşıyan sürüklenebilir öğe örneğinin aksine, bu uygulama, alt bileşen "seçici"yi renk seçici üzerinde taşımak için 2D deltaları kullanır:

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

Video 5. Seçilen renk değerlerini güncellemek için 2D deltaların kapsayıcının sınırlarına nasıl sabitlendiğini gösteren, her yöne sürüklenebilen beyaz dairesel seçici düğmeli renk gradyanı.

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

  • Gradyan kapsayıcının gerçek boyutlarını yakalamak için onSizeChanged değiştiricisini kullanır. Seçici, kenarların tam olarak nerede olduğunu bilir.
  • graphicsLayer içinde, sürükleme sırasında seçicinin ortada kalmasını sağlamak için translationX ve translationY değerlerini ayarlar.