ลากคอมโพสิเบิล

การอนุญาตให้ผู้ใช้ลากคอมโพเนนต์ข้ามหน้าจอด้วยตนเองเป็นสิ่งสำคัญสำหรับแอป Android สมัยใหม่ ไม่ว่าคุณจะสร้างแถบเลื่อนที่กำหนดเอง คอมโพเนนต์ปัดเพื่อปิด หรือ Canvas แบบอินเทอร์แอกทีฟ Jetpack Compose มีชุด API ที่ครอบคลุมเพื่อจัดการเหตุการณ์การลากได้อย่างง่ายดาย

API การลากของ Compose จะช่วยลดความซับซ้อนของการโต้ตอบแบบสัมผัสแบบดิบ แทนที่จะคำนวณพิกัดการสัมผัสและจัดการวงจรการใช้งานท่าทางสัมผัส ด้วย pointerInput ด้วยตนเอง คุณสามารถใช้ตัวปรับแต่งในตัวที่เชื่อมโยง โดยตรงกับระบบการจัดการสถานะและภาพเคลื่อนไหวของ Compose ได้

หน้านี้อธิบายวิธีและเวลาที่จะใช้ API การลากแต่ละรายการ

เลือก API การลากที่ถูกต้อง

มี API หลายรายการที่คุณใช้เพื่อติดตั้งใช้งานเนื้อหาที่ลากได้

  • เลือกคอมโพเนนต์ Material มาตรฐาน (เช่น SwipeToDismissBox หรือลิ้นชักการนำทาง) ในกรณีต่อไปนี้: คุณต้องการลักษณะการทำงานการปัดหรือการลาก Material 3 เริ่มต้น (เช่น การลบอีเมล การเก็บถาวรงาน หรือการเปิดเมนูด้านข้าง / Bottom Sheet) คอมโพเนนต์เหล่านี้มีลักษณะการทำงานในตัวโดยไม่จำเป็นต้องมีการจัดการสถานะหรือเลย์เอาต์ที่กำหนดเอง
  • เลือก Modifier.draggable ในกรณีต่อไปนี้: คุณต้องการเฉพาะค่าเดลต้าการลากแบบดิบ (เช่น การลากแถบเลื่อนที่กำหนดเอง) ดูข้อมูลเพิ่มเติมได้ที่ การลากตามแกนเดียวด้วย `draggable`
    • เลือก Modifier.draggable2D ในกรณีต่อไปนี้: คุณต้องการให้คอมโพเนนต์ เคลื่อนที่อย่างต่อเนื่องตามแกน X และแกน Y พร้อมกัน (เช่น การเลื่อนดูแผนที่หรือการสร้างปุ่มการทำงานแบบลอยที่ผู้ใช้ลากไปที่ใดก็ได้บนหน้าจอ) ดูข้อมูลเพิ่มเติมได้ที่ การลากใน 2 มิติด้วย Modifier.draggable2D
  • เลือก Modifier.anchoredDraggable ในกรณีต่อไปนี้: คุณกำลังสร้าง คอมโพเนนต์แบบอินเทอร์แอกทีฟที่กำหนดเองซึ่งต้อง Snap ระหว่างตำแหน่งที่แยกกัน 2 ตำแหน่งขึ้นไป (เช่น Bottom Sheet, ลิ้นชักการนำทาง หรือการดำเนินการปัดเพื่อแสดง) ดูข้อมูลเพิ่มเติมได้ที่ การลากคอมโพเนนต์ระหว่าง จุดยึดด้วย 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
                    }
                }
        )
    }
}

แทรกข้อความแสดงแทนที่นี่
รูปที่ 1 องค์ประกอบ UI ที่นิ้วกำลังลาก

การลากคอมโพเนนต์ระหว่างจุดยึดด้วย anchoredDraggable

ตัวปรับแต่ง anchoredDraggable ช่วยให้คุณลากคอมโพเนนต์ระหว่างตำแหน่งที่ กำหนดไว้ล่วงหน้าซึ่งเรียกว่าจุดยึดได้ เมื่อผู้ใช้ปล่อยท่าทางสัมผัสการลาก คอมโพเนนต์จะหยุดที่จุดยึดที่ใกล้ที่สุด anchoredDraggable มีประโยชน์สำหรับการสร้างคอมโพเนนต์ เช่น Bottom Sheet ที่กำหนดเอง แถบเลื่อนที่ซับซ้อน หรือแผง Drawer

แพลตฟอร์ม API

API AnchoredDraggable ประกอบด้วยส่วนหลักๆ ดังนี้

  • จุดยึด (DraggableAnchors): ตัวสร้างที่กำหนดการแมประหว่างสถานะที่กำหนดเอง (เช่น คลาส Enum สำหรับ Start, Center, End) กับค่าออฟเซ็ตพิกเซลที่เกี่ยวข้อง
  • สถานะ (AnchoredDraggableState<T>): ตัวเก็บสถานะที่จัดการตำแหน่งปัจจุบัน (ออฟเซ็ต) ของคอมโพเนนต์ จุดยึดที่คอมโพเนนต์หยุด และความคืบหน้าการลาก ควรสร้างโดยใช้ rememberSaveable และ AnchoredDraggableState.Saver<T> เพื่อรักษาสถานะไว้เมื่อมีการเปลี่ยนแปลงการกำหนดค่า
  • ตัวปรับแต่งท่าทางสัมผัส (Modifier.anchoredDraggable): ตัวปรับแต่งที่ตรวจจับท่าทางสัมผัสการลากตาม Orientation อัปเดตสถานะด้วยค่าเดลต้าการลาก และจัดการท่าทางสัมผัสการปัดเพื่อแสดงภาพเคลื่อนไหวของคอมโพเนนต์ไปยังจุดยึดที่ใกล้ที่สุดเมื่อปล่อย

ตัวอย่าง: สร้างคอมโพเนนต์ SwipeToDismiss ที่กำหนดเอง

ตัวอย่างนี้แสดงวิธีสร้างคอมโพเนนต์ SwipeToDismiss ที่กำหนดเอง (แบนเนอร์สีขาว) ที่ผู้ใช้ปัดไปทางซ้ายเพื่อปิด หรือลากไปทางขวาเพื่อดูเอฟเฟกต์การยืดเมื่อเลื่อนเลย

  1. กำหนดค่าจุดยึด กำหนด Enum ที่แสดงตำแหน่งที่แยกกันซึ่งเนื้อหาที่ลากได้จะหยุด

    private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }

  2. สร้างอินสแตนซ์และบันทึก AnchoredDraggableState ใช้ rememberSaveable กับ AnchoredDraggableState.Saver() เพื่อคงตำแหน่งปัจจุบันของคอมโพเนนต์ไว้เมื่อมีการสร้างกิจกรรมใหม่และการเปลี่ยนแปลงการกำหนดค่า (เช่น การหมุนหน้าจอหรือการเปลี่ยนแปลงธีม)

    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() ภายในแลมบดาออฟเซ็ต การอ่านออฟเซ็ตภายในแลมบดานี้จะช่วยให้มั่นใจได้ว่าระบบจะอ่านค่าในระยะการวางเท่านั้น ซึ่งจะช่วยหลีกเลี่ยงการจัดองค์ประกอบใหม่ที่ไม่จำเป็นระหว่างการลาก ใช้ตัวปรับแต่งนี้กับเนื้อหาที่ควรเคลื่อนไหวด้วยสายตา

    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. [ไม่บังคับ] ผสานรวมการเลื่อนเกิน API overscrollEffect จะติดตั้งใช้งานเอฟเฟกต์ภาพที่แสดงขึ้นเมื่อมีการเลื่อนหรือปัดไปถึงขอบของคอนเทนเนอร์ที่เลื่อนได้ คุณสามารถแสดงความคิดเห็นด้วยภาพเมื่อผู้ใช้ลากเลยขอบเขตจุดยึดที่กำหนดไว้ด้วยเอฟเฟกต์การเลื่อนเลย (โดยปกติจะเป็นการยืดหรือเรืองแสง)

    1. ส่ง: ส่งอินสแตนซ์ overscrollEffect ไปยังตัวปรับแต่ง anchoredDraggable ซึ่งจะส่งค่าเดลต้าการลากไปยังสถานะ 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 การติดตั้งใช้งานคอมโพเนนต์ปัดเพื่อปิดที่กำหนดเอง

ตัวอย่าง: สร้างลิ้นชักการนำทางที่กำหนดเองซึ่งขับเคลื่อนด้วยท่าทางสัมผัส

ตัวอย่างนี้แสดงวิธีสร้าง Navigation Drawer ที่กำหนดเองซึ่งขับเคลื่อนด้วยท่าทางสัมผัส เมื่อผู้ใช้ลากในแนวนอน หน้าจอเบื้องหน้า (ScreenContents) จะเลื่อนไปทางขวา ลดขนาดจาก 1.0f เป็น 0.8f และปัดมุมเพื่อแสดง Navigation Drawer ที่อยู่ด้านล่าง

  1. กำหนดสถานะและตำแหน่งจุดยึด ในตัวอย่างนี้ จุดยึดคือสถานะเปิดและปิดของ Drawer กำหนด Enum สำหรับสถานะ Open และ Closed แล้วแมปสถานะเหล่านั้นกับออฟเซ็ตพิกเซลโดยใช้ 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 ไว้ที่ด้านบน Drawer จะยังคงอยู่กับที่ ขณะที่เลเยอร์บนสุดจะเลื่อนและปรับขนาดเหนือ Drawer

    เมื่อผู้ใช้เลือกรายการ Drawer ให้อัปเดตสถานะหน้าจอที่ใช้งานอยู่และปิด Drawer โดยอัตโนมัติโดยใช้ 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() เพื่อรับออฟเซ็ตการลากปัจจุบัน
    • ใช้ lerp() (การประมาณค่าเชิงเส้น) เพื่อคำนวณการปรับขนาดตามสัดส่วน (1f ถึง 0.8f) และรัศมีมุม (0.dp ถึง 32.dp) เทียบกับออฟเซ็ตและความกว้างของ Drawer
    • 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 ควบคุม Drawer แบบเป็นโปรแกรม (เช่น เมื่อแตะไอคอนเมนูหรือเลือกรายการ Drawer) โดยเปิดใช้ 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)
            }
        }
    }

ผลลัพธ์

วิดีโอ 3 หน้าจอเบื้องหน้า (Sleep) ถูกลากไปทางขวา ซึ่งจะแสดงลิ้นชักการนำทางที่อยู่ด้านล่าง

การลากใน 2 มิติด้วย Modifier.draggable2D

ใช้ตัวปรับแต่ง draggable2D เพื่อย้ายองค์ประกอบ UI แต่ละรายการตามแกน X และแกน Y พร้อมกัน

ลากองค์ประกอบที่สร้างได้

ตัวอย่างนี้แสดง Use Case ที่พบบ่อยที่สุดสำหรับ draggable2D นั่นคือ การอนุญาตให้ผู้ใช้หยิบองค์ประกอบ UI และเปลี่ยนตำแหน่งองค์ประกอบนั้นที่ใดก็ได้ภายในคอนเทนเนอร์หลัก

@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 กล่องสีม่วงขนาดเล็กถูกเปลี่ยนตำแหน่งบนพื้นหลังสีเทา ซึ่งแสดงการลาก 2 มิติโดยตรงที่องค์ประกอบจะหยุดเคลื่อนที่ทันทีที่ผู้ใช้ยกนิ้วขึ้น

ข้อมูลโค้ดก่อนหน้ามีข้อมูลต่อไปนี้

  • ติดตามตำแหน่งของกล่องโดยใช้สถานะ offset
  • ใช้ตัวปรับแต่ง offset เพื่อเปลี่ยนตำแหน่งของคอมโพเนนต์ตามค่าเดลต้าการลาก
  • เนื่องจากไม่มีการรองรับการปัด กล่องจึงหยุดเคลื่อนที่ทันทีที่ผู้ใช้ยกนิ้วขึ้น

ลากองค์ประกอบที่สร้างได้ย่อยตามพื้นที่การลากขององค์ประกอบหลัก

ตัวอย่างนี้แสดงวิธีใช้ draggable2D เพื่อสร้างพื้นที่ป้อนข้อมูล 2 มิติที่ปุ่มเลือกถูกจำกัดไว้ภายในพื้นผิวที่เฉพาะเจาะจง การติดตั้งใช้งานนี้ใช้ค่าเดลต้า 2 มิติเพื่อย้าย "ตัวเลือก" องค์ประกอบที่สร้างได้ย่อยไปทั่วตัวเลือกสี ซึ่งแตกต่างจากตัวอย่างองค์ประกอบที่ลากได้ซึ่งย้ายคอมโพเนนต์เอง

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

วิดีโอ 5 การไล่ระดับสีพร้อมปุ่มเลือกวงกลมสีขาวที่ลากได้ทุกทิศทาง ซึ่งแสดงวิธีหนีบค่าเดลต้า 2 มิติไว้ที่ขอบเขตของคอนเทนเนอร์เพื่ออัปเดตค่าสีที่เลือก

ข้อมูลโค้ดก่อนหน้ามีข้อมูลต่อไปนี้

  • ใช้ตัวปรับแต่ง onSizeChanged เพื่อบันทึกขนาดจริงของคอนเทนเนอร์การไล่ระดับสี ตัวเลือกจะทราบตำแหน่งขอบที่แน่นอน
  • ภายใน graphicsLayer ตัวเลือกจะปรับ translationX และ translationY เพื่อให้แน่ใจว่าตัวเลือกจะอยู่ตรงกลางขณะลาก