कंपोज़ेबल को खींचें और छोड़ें

उपयोगकर्ताओं को स्क्रीन पर कॉम्पोनेंट को खींचकर छोड़ने की सुविधा देना, मॉडर्न Android ऐप्लिकेशन के लिए ज़रूरी है. चाहे आपको कस्टम स्लाइडर, स्वाइप-टू-डिसमिस कॉम्पोनेंट या इंटरैक्टिव कैनवस बनाना हो, Jetpack Compose में एपीआई का एक पूरा सेट मिलता है. इससे ड्रैग इवेंट को आसानी से मैनेज किया जा सकता है.

Compose के ड्रैगिंग एपीआई, रॉ टच इवेंट की जटिलता को कम करते हैं. pointerInput की मदद से, टच कोऑर्डिनेट का मैन्युअल तरीके से हिसाब लगाने और जेस्चर की लाइफ़साइकल को मैनेज करने के बजाय, पहले से मौजूद मॉडिफ़ायर का इस्तेमाल किया जा सकता है. ये मॉडिफ़ायर, Compose के स्टेट मैनेजमेंट और ऐनिमेशन सिस्टम से सीधे तौर पर जुड़े होते हैं.

इस पेज पर, हर ड्रैगिंग एपीआई को इस्तेमाल करने का तरीका और समय बताया गया है.

ड्रैग करने के लिए सही एपीआई चुनना

खींचने और छोड़ने लायक कॉन्टेंट लागू करने के लिए, कई एपीआई इस्तेमाल किए जा सकते हैं.

  • स्टैंडर्ड Material कॉम्पोनेंट (जैसे, SwipeToDismissBox या नेविगेशन पैनल) तब चुनें, जब: आपको Material 3 के डिफ़ॉल्ट स्वाइप या ड्रैग बिहेवियर की ज़रूरत हो. उदाहरण के लिए, कोई ईमेल मिटाना, किसी टास्क को संग्रहित करना या साइड मेन्यू / बॉटम शीट खोलना. इनमें पहले से मौजूद व्यवहार होते हैं. इनके लिए, कस्टम स्टेट या लेआउट मैनेजमेंट की ज़रूरत नहीं होती.
  • Modifier.draggable को तब चुनें: जब आपको सिर्फ़ रॉ ड्रैग डेल्टा की ज़रूरत हो. उदाहरण के लिए, कस्टम स्लाइडर को खींचकर छोड़ना. ज़्यादा जानकारी के लिए, खींचने और छोड़ने लायक एलिमेंट की मदद से, किसी एक ऐक्सिस पर खींचें और छोड़ें लेख पढ़ें.
    • Modifier.draggable2DModifier.draggable2D कब: आपको किसी कॉम्पोनेंट को X और Y, दोनों ऐक्सिस पर एक साथ लगातार मूव करना हो. उदाहरण के लिए, किसी मैप पर पैन करना या फ़्लोट करने वाला ऐसा कार्रवाई बटन बनाना जिसे उपयोगकर्ता स्क्रीन पर कहीं भी खींचकर छोड़ सके. ज़्यादा जानकारी के लिए, Modifier.draggable2D की मदद से दो डाइमेंशन में खींचें और छोड़ें लेख पढ़ें.
  • Modifier.anchoredDraggable को तब चुनें: जब आपको इंटरैक्टिव कॉम्पोनेंट बनाने हों. ये कॉम्पोनेंट, दो या इससे ज़्यादा अलग-अलग पोज़िशन के बीच स्नैप होने चाहिए. उदाहरण के लिए, बॉटम शीट, नेविगेशन पैनल या स्वाइप-टू-रिवील ऐक्शन. ज़्यादा जानकारी के लिए, anchoredDraggable की मदद से, कॉम्पोनेंट को ऐंकर पॉइंट के बीच खींचें और छोड़ें लेख पढ़ें.

draggable की मदद से, किसी एक ऐक्सिस पर खींचें और छोड़ें

draggable मॉडिफ़ायर, एक ही ओरिएंटेशन में जेस्चर को खींचने और छोड़ने के लिए हाई-लेवल एंट्री पॉइंट है. साथ ही, यह खींचने और छोड़ने की दूरी को पिक्सल में दिखाता है.

ध्यान दें कि यह मॉडिफ़ायर scrollable की तरह ही है. यह सिर्फ़ जेस्चर का पता लगाता है. आपको स्टेट को होल्ड करना होगा और उसे स्क्रीन पर दिखाना होगा. उदाहरण के लिए, offset मॉडिफ़ायर का इस्तेमाल करके एलिमेंट को मूव करें:

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

अगर आपको खींचने और छोड़ने के पूरे जेस्चर को कंट्रोल करना है, तो pointerInput मॉडिफ़ायर का इस्तेमाल करके, खींचने और छोड़ने के जेस्चर का पता लगाने वाले टूल का इस्तेमाल करें.

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

वैकल्पिक टेक्स्ट यहां डालें
पहली इमेज. एक उंगली से खींचे जा रहे यूज़र इंटरफ़ेस (यूआई) एलिमेंट की इमेज

anchoredDraggable की मदद से, कॉम्पोनेंट को ऐंकर पॉइंट के बीच खींचें और छोड़ें

anchoredDraggable मॉडिफ़ायर की मदद से, किसी कॉम्पोनेंट को पहले से तय की गई पोज़िशन के सेट के बीच खींचा जा सकता है. इन पोज़िशन को ऐंकर पॉइंट कहा जाता है. जब उपयोगकर्ता खींचने और छोड़ने का जेस्चर रिलीज़ करता है, तो कॉम्पोनेंट सबसे नज़दीकी ऐंकर पर सेट हो जाता है. anchoredDraggable का इस्तेमाल, कस्टम बॉटम शीट, कॉम्प्लेक्स स्लाइडर या ड्रॉअर पैनल जैसे कॉम्पोनेंट बनाने के लिए किया जाता है.

एपीआई सरफ़ेस

AnchoredDraggable API में ये मुख्य हिस्से शामिल हैं:

  • ऐंकर (DraggableAnchors): एक बिल्डर, जो आपकी कस्टम स्थितियों (जैसे, Start, Center, End के लिए एक enum क्लास) और उनसे जुड़ी पिक्सल ऑफ़सेट वैल्यू के बीच मैपिंग तय करता है.
  • State (AnchoredDraggableState<T>): यह एक स्टेट होल्डर है, जो कॉम्पोनेंट की मौजूदा पोज़िशन (ऑफ़सेट), उसके ऐंकर, और उसे खींचने और छोड़ने की प्रोग्रेस को मैनेज करता है. इसे rememberSaveable और AnchoredDraggableState.Saver<T> का इस्तेमाल करके बनाया जाना चाहिए, ताकि कॉन्फ़िगरेशन में बदलाव होने पर भी इसकी स्थिति बनी रहे.
  • जेस्चर मॉडिफ़ायर (Modifier.anchoredDraggable): यह एक ऐसा मॉडिफ़ायर है जो Orientation के साथ ड्रैग जेस्चर का पता लगाता है. साथ ही, ड्रैग डेल्टा के साथ स्थिति को अपडेट करता है. इसके अलावा, यह फ़्लिंग जेस्चर को हैंडल करता है, ताकि रिलीज़ होने पर कॉम्पोनेंट को सबसे नज़दीकी ऐंकर पर ऐनिमेट किया जा सके.

उदाहरण: कस्टम SwipeToDismiss कॉम्पोनेंट बनाना

इस उदाहरण में, कस्टम SwipeToDismiss कॉम्पोनेंट (सफ़ेद बैनर) बनाने का तरीका बताया गया है. इसे उपयोगकर्ता, बाईं ओर स्वाइप करके खारिज कर सकता है या दाईं ओर खींचकर ओवरस्क्रोल स्ट्रेच इफ़ेक्ट देख सकता है.

  1. ऐंकर वैल्यू तय करें. खींचने और छोड़ने लायक कॉन्टेंट को जिन अलग-अलग पोज़िशन पर सेटल किया जा सकता है उन्हें दिखाने के लिए, एक enum तय करें:

    private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }

  2. AnchoredDraggableState को इंस्टैंशिएट करें और सेव करें. गतिविधि को फिर से बनाने और कॉन्फ़िगरेशन में बदलाव करने (जैसे, स्क्रीन को घुमाना या थीम बदलना) के दौरान, कॉम्पोनेंट की मौजूदा पोज़िशन को बनाए रखने के लिए, AnchoredDraggableState.Saver() के साथ rememberSaveable का इस्तेमाल करें:

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

  3. layout, offset, और draggable मॉडिफ़ायर लागू करें. कॉम्पोनेंट को खींचने और छोड़ने लायक बनाने और यह पक्का करने के लिए कि उसके ऐंकर पॉइंट उसके साइज़ के हिसाब से अडजस्ट हों, उस कॉन्टेंट पर ये मॉडिफ़ायर लागू करें जिसे विज़ुअल तौर पर मूव करना है:

    • Modifier.layout: ऐंकर की पोज़िशन अक्सर कॉम्पोनेंट के साइज़ पर निर्भर करती हैं. शुरुआती कंपोज़िशन के दौरान, कॉम्पोनेंट के साइज़ की जानकारी नहीं होती. कॉम्पोनेंट को मेज़र करने के लिए, layout मॉडिफ़ायर का इस्तेमाल करें. साथ ही, कॉम्पोनेंट की चौड़ाई के आधार पर, डाइनैमिक तौर पर DraggableAnchors मैप का हिसाब लगाएं. इसके बाद, state.updateAnchors() का इस्तेमाल करके स्टेटस को अपडेट करें.

    • Modifier.anchoredDraggable: यह स्टेट से कनेक्ट करता है, तय किए गए ओरिएंटेशन में ड्रैग डेल्टा भेजता है, और फ़्लिंग जेस्चर को हैंडल करता है, ताकि रिलीज़ होने पर कॉम्पोनेंट को सबसे नज़दीकी ऐंकर पर ऐनिमेट किया जा सके. इस मॉडिफ़ायर को उस कॉम्पोनेंट में जोड़ें जो खींचने और छोड़ने के जेस्चर का जवाब दे. साथ ही, इसे state और खींचने और छोड़ने के लिए orientation (और, वैकल्पिक तौर पर, overscrollEffect) पास करें.
    • Modifier.offset: यह ऑफ़सेट लैम्डा के अंदर state.requireOffset() से ऑफ़सेट को पढ़ता है. इस lambda फ़ंक्शन में ऑफ़सेट को पढ़ने से यह पक्का होता है कि वैल्यू सिर्फ़ प्लेसमेंट फ़ेज़ के दौरान पढ़ी जाए. इससे, खींचने और छोड़ने के दौरान, ग़ैर-ज़रूरी रीकंपोज़िशन से बचा जा सकता है. इस मॉडिफ़ायर को उस कॉन्टेंट पर लागू करें जिसे विज़ुअल तौर पर मूव करना है.

    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. [ज़रूरी नहीं] ओवरस्क्रोल को इंटिग्रेट करें. overscrollEffect एपीआई, एक विज़ुअल इफ़ेक्ट लागू करता है. यह इफ़ेक्ट तब दिखता है, जब स्क्रोल या फ़्लिंग करके स्क्रोलिंग कंटेनर के किनारों तक पहुंचा जाता है. जब कोई उपयोगकर्ता, तय की गई ऐंकर सीमाओं से आगे ड्रैग करता है, तो उसे विज़ुअल फ़ीडबैक दिया जा सकता है. इसके लिए, ओवरस्क्रोल इफ़ेक्ट (आम तौर पर, स्ट्रेच या ग्लो) का इस्तेमाल किया जा सकता है.

    1. डिस्पैच: anchoredDraggable मॉडिफ़ायर को overscrollEffect इंस्टेंस पास करें. यह ड्रैग डेल्टा को AnchoredDraggable स्टेट या ओवरस्क्रोल इफ़ेक्ट पर भेजता है. यह इस बात पर निर्भर करता है कि जेस्चर, ऐंकर बाउंड के अंदर है या बाहर.
    2. रेंडर करना: Modifier.overscroll को उसी OverscrollEffect इंस्टेंस के साथ उस कॉम्पोनेंट पर लागू करें जिसे इफ़ेक्ट को विज़ुअल तौर पर रेंडर करना चाहिए.

नीचे दिए गए स्निपेट में, स्टैंडर्ड स्ट्रेच इफ़ेक्ट जोड़ने का तरीका बताया गया है:

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

नतीजा

ऊपर दिए गए कोड स्निपेट से, सफ़ेद रंग का बैनर बनता है. इसमें "मुझे खारिज करने के लिए खींचें और छोड़ें!" टेक्स्ट दिखता है. उपयोगकर्ता, इसे खारिज करने के लिए बाईं ओर स्वाइप कर सकता है या ओवरस्क्रोल स्ट्रेच इफ़ेक्ट देखने के लिए दाईं ओर खींच सकता है:

वीडियो 2. स्वाइप करके खारिज करने वाले कस्टम कॉम्पोनेंट को लागू करने का तरीका.

उदाहरण: जेस्चर से कंट्रोल होने वाला कस्टम नेविगेशन पैनल बनाना

इस उदाहरण में, जेस्चर से कंट्रोल होने वाला कस्टम नेविगेशन पैनल बनाने का तरीका बताया गया है. जब उपयोगकर्ता हॉरिज़ॉन्टल तौर पर खींचता है, तो फ़ोरग्राउंड स्क्रीन (ScreenContents) दाईं ओर स्लाइड होती है. साथ ही, 1.0f से 0.8f तक स्केल डाउन होती है और इसके कोने गोल हो जाते हैं, ताकि नीचे मौजूद नेविगेशन पैनल दिख सके.

  1. ऐंकर की स्थितियों और जगहों को तय करें. इस उदाहरण में, ऐंकर, ड्रॉअर के खुले और बंद होने की स्थितियां हैं. Open और Closed स्थितियों के लिए एक enum तय करें. इसके बाद, DraggableAnchors का इस्तेमाल करके उन्हें पिक्सल ऑफ़सेट पर मैप करें:

    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. किसी आइटम पर क्लिक करने पर, anchoredDraggableState को ऐनिमेट करना. HomeScreenDrawer को Box के सबसे नीचे और ScreenContents को सबसे ऊपर रखें. ड्रॉअर अपनी जगह पर रहता है, जबकि सबसे ऊपर की लेयर उसके ऊपर मूव और स्केल होती है.

    जब कोई उपयोगकर्ता ड्रॉअर का कोई आइटम चुनता है, तो चालू स्क्रीन की स्थिति को अपडेट करें और state.animateTo(DrawerState.Closed) का इस्तेमाल करके, ड्रॉअर को अपने-आप खारिज करें:

    @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. एक से ज़्यादा विज़ुअल प्रॉपर्टी बदलने के लिए, state.requireOffset() का इस्तेमाल करें. सिर्फ़ कॉन्टेंट को ऑफ़सेट करने के बजाय, Modifier.graphicsLayer की मदद से कई प्रॉपर्टी बदली जा सकती हैं. इससे, ड्रैग करने की प्रोग्रेस के आधार पर, ट्रांसलेशन, स्केल, और कोने के रेडियस को एक साथ ऐनिमेट किया जा सकता है:

    • मौजूदा ड्रैग ऑफ़सेट पाने के लिए, state.requireOffset() का इस्तेमाल करें.
    • ऑफ़सेट और ड्रॉअर की चौड़ाई के हिसाब से, अनुपात के हिसाब से स्केल (1f से 0.8f) और कोने की त्रिज्या (0.dp से 32.dp) का हिसाब लगाने के लिए, lerp() (लीनियर इंटरपोलेशन) का इस्तेमाल करें.
    • Modifier.graphicsLayer, ड्रॉ फ़ेज़ में बदलावों को लागू करता है. इससे, ड्रैग करने के दौरान रीकंपोज़िशन को ट्रिगर किए बिना, बेहतर परफ़ॉर्मेंस वाले ऐनिमेशन मिलते हैं.

    @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. animateTo की मदद से, प्रोग्राम के हिसाब से ऐनिमेशन ट्रिगर करें. ड्रॉअर को प्रोग्राम के हिसाब से कंट्रोल करें (जैसे कि मेन्यू आइकॉन पर टैप करते समय या ड्रॉअर आइटम चुनते समय). इसके लिए, कोरूटीन स्कोप में state.animateTo() को लॉन्च करें:

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

नतीजा

तीसरा वीडियो. फ़ोरग्राउंड स्क्रीन (स्लीप) को दाईं ओर खींचकर छोड़ा गया है. इससे, नीचे मौजूद नेविगेशन पैनल दिख रहा है.

Modifier.draggable2D की मदद से, दो डाइमेंशन में खींचें और छोड़ें

X और Y, दोनों ऐक्सिस पर अलग-अलग यूज़र इंटरफ़ेस (यूआई) एलिमेंट को एक साथ मूव करने के लिए, 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)
        }
    }
}

वीडियो 4. स्लेटी रंग के बैकग्राउंड पर, बैंगनी रंग के एक छोटे बॉक्स की जगह बदली जा रही है. इससे, डायरेक्ट 2D ड्रैगिंग की सुविधा के बारे में पता चलता है. इसमें, उपयोगकर्ता के उंगली हटाते ही एलिमेंट की जगह बदलना बंद हो जाता है.

ऊपर दिए गए कोड स्निपेट में ये शामिल हैं:

  • offset स्टेट का इस्तेमाल करके, बॉक्स की पोज़िशन को ट्रैक करता है.
  • यह कॉम्पोनेंट की पोज़िशन को ड्रैग डेल्टा के आधार पर शिफ़्ट करने के लिए, offset मॉडिफ़ायर का इस्तेमाल करता है.
  • फ़्लिंग की सुविधा न होने की वजह से, उपयोगकर्ता के उंगली हटाते ही बॉक्स हिलना बंद हो जाता है.

पैरंट के ड्रैग एरिया के आधार पर, चाइल्ड कंपोज़ेबल को खींचें और छोड़ें

इस उदाहरण में, draggable2D का इस्तेमाल करके 2D इनपुट एरिया बनाने का तरीका बताया गया है. इसमें सिलेक्टर नॉब को किसी खास सरफ़ेस तक सीमित किया गया है. खींचने और छोड़ने लायक एलिमेंट के उदाहरण में, कॉम्पोनेंट को ही मूव किया जाता है. हालांकि, इस उदाहरण में 2D डेल्टा का इस्तेमाल करके, चाइल्ड कंपोज़ेबल "सिलेक्टर" को कलर पिकर पर मूव किया जाता है:

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

पांचवां वीडियो. सफ़ेद रंग के गोल सिलेक्टर नॉब वाला कलर ग्रेडिएंट, जिसे किसी भी दिशा में खींचा जा सकता है. इससे पता चलता है कि चुने गए रंग की वैल्यू को अपडेट करने के लिए, 2D डेल्टा को कंटेनर की सीमाओं में कैसे क्लैंप किया जाता है.

पिछले स्निपेट में ये शामिल हैं:

  • यह ग्रेडिएंट कंटेनर के असल डाइमेंशन को कैप्चर करने के लिए, onSizeChanged मॉडिफ़ायर का इस्तेमाल करता है. सिलेक्टर को पता होता है कि किनारों को कहां से चुनना है.
  • graphicsLayer के अंदर, यह translationX और translationY को अडजस्ट करता है, ताकि खींचते समय सिलेक्टर बीच में रहे.