การอนุญาตให้ผู้ใช้ลากคอมโพเนนต์ไปมาบนหน้าจอเป็นสิ่งสำคัญสำหรับแอป 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เมื่อ: คุณกำลังสร้างคอมโพเนนต์แบบอินเทอร์แอกทีฟที่กำหนดเองซึ่งต้องสแนประหว่างตำแหน่งที่ไม่ต่อเนื่องกัน 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 } } ) } }
ลากคอมโพเนนต์ระหว่างจุดยึดด้วย anchoredDraggable
ตัวแก้ไข anchoredDraggable ช่วยให้คุณลากคอมโพเนนต์ระหว่างชุดตำแหน่งที่กำหนดไว้ล่วงหน้าซึ่งเรียกว่าจุดยึดได้ เมื่อผู้ใช้ปล่อยท่าทางการลาก คอมโพเนนต์จะหยุดที่ Anchor ที่ใกล้ที่สุด anchoredDraggable มีประโยชน์สำหรับการสร้างคอมโพเนนต์ เช่น Bottom Sheet ที่กำหนดเอง แถบเลื่อนที่ซับซ้อน หรือแผงลิ้นชัก
แพลตฟอร์ม API
AnchoredDraggable API ประกอบด้วยส่วนหลักๆ ต่อไปนี้
- Anchors (
DraggableAnchors): ตัวสร้างที่กำหนดการจับคู่ระหว่างสถานะที่กำหนดเอง (เช่น คลาส enum สำหรับStart, Center, End) กับค่าออฟเซ็ตพิกเซลที่เกี่ยวข้อง - State (
AnchoredDraggableState<T>): ตัวเก็บสถานะที่จัดการตำแหน่งปัจจุบัน (ออฟเซ็ต) ของคอมโพเนนต์ จุดยึดที่คอมโพเนนต์อยู่ และความคืบหน้าในการลาก ควรสร้างโดยใช้rememberSaveableและAnchoredDraggableState.Saver<T>เพื่อรักษาสถานะไว้เมื่อมีการเปลี่ยนแปลงการกำหนดค่า - ตัวปรับแต่งท่าทางสัมผัส (
Modifier.anchoredDraggable): ตัวปรับแต่งที่ตรวจจับท่าทางสัมผัสการลากตามOrientationอัปเดตสถานะด้วยเดลต้าการลาก และจัดการท่าทางสัมผัสการเหวี่ยงเพื่อทำให้คอมโพเนนต์เคลื่อนไหวไปยัง Anchor ที่ใกล้ที่สุดเมื่อปล่อย
ตัวอย่าง: สร้างคอมโพเนนต์ SwipeToDismiss ที่กำหนดเอง
ตัวอย่างนี้แสดงวิธีสร้างคอมโพเนนต์ SwipeToDismiss ที่กำหนดเอง (แบนเนอร์สีขาว) ซึ่งผู้ใช้สามารถปัดไปทางซ้ายเพื่อปิด หรือลากไปทางขวาเพื่อดูเอฟเฟกต์การยืดแบบการเลื่อนเกินได้
กำหนดค่า Anchor กำหนด enum ที่แสดงถึงตำแหน่งที่ไม่ต่อเนื่องซึ่งเนื้อหาที่ลากได้สามารถวางได้ ดังนี้
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
สร้างอินสแตนซ์และบันทึก
AnchoredDraggableStateใช้rememberSaveableกับAnchoredDraggableState.Saver()เพื่อคงตำแหน่งปัจจุบันของคอมโพเนนต์ไว้ตลอดการสร้างกิจกรรมใหม่และการเปลี่ยนแปลงการกำหนดค่า (เช่น การหมุนหน้าจอหรือการเปลี่ยนธีม)val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting) }
ใช้ตัวแก้ไข
layout,offsetและdraggableหากต้องการทำให้คอมโพเนนต์ลากได้และตรวจสอบว่าจุดยึดปรับตามขนาดของคอมโพเนนต์ ให้ใช้ตัวแก้ไขต่อไปนี้กับเนื้อหาที่ควรเคลื่อนไหวในเชิงภาพModifier.layout: ตำแหน่ง Anchor มักจะขึ้นอยู่กับขนาดของคอมโพเนนต์ ซึ่งไม่ทราบในระหว่างการคอมโพสิชันเริ่มต้น ใช้ตัวปรับแต่งlayoutเพื่อวัดคอมโพเนนต์ คำนวณแผนที่DraggableAnchorsแบบไดนามิกตามความกว้าง และอัปเดตสถานะโดยใช้state.updateAnchors()
Modifier.anchoredDraggable: เชื่อมต่อกับสถานะ ส่งเดลต้าการลากในทิศทางที่ระบุ และจัดการท่าทางสัมผัสการเหวี่ยงเพื่อทำให้คอมโพเนนต์เคลื่อนไหวไปยัง Anchor ที่ใกล้ที่สุดเมื่อปล่อย เพิ่มตัวแก้ไขนี้ลงในคอมโพเนนต์ที่ควรตอบสนองต่อท่าทางการลากและส่ง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() }
[ไม่บังคับ] ผสานรวมการเลื่อนเกิน
overscrollEffectAPI ใช้เอฟเฟกต์ภาพที่แสดงเมื่อเลื่อนหรือเหวี่ยงไปถึงขอบของคอนเทนเนอร์ที่เลื่อนได้ คุณสามารถแสดงภาพเพื่อตอบสนองเมื่อผู้ใช้ลากผ่านขอบเขต Anchor ที่กำหนดด้วยเอฟเฟกต์การเลื่อนเกิน (โดยปกติจะเป็นการยืดหรือเรืองแสง)- Dispatch: ส่งอินสแตนซ์
overscrollEffectไปยังตัวแก้ไขanchoredDraggableซึ่งจะส่งเดลต้าการลากไปยังสถานะAnchoredDraggableหรือเอฟเฟกต์การเลื่อนเกิน ขึ้นอยู่กับว่าท่าทางสัมผัสอยู่ภายในหรือภายนอกขอบเขต Anchor - แสดงผล: ใช้
Modifier.overscrollกับอินสแตนซ์OverscrollEffectเดียวกันกับคอมโพเนนต์ที่ควรแสดงผลเอฟเฟกต์เป็นภาพ
- Dispatch: ส่งอินสแตนซ์
ข้อมูลโค้ดต่อไปนี้แสดงวิธีเพิ่มเอฟเฟกต์การยืดมาตรฐาน
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() } }
ผลลัพธ์
ข้อมูลโค้ดก่อนหน้าจะแสดงแบนเนอร์สีขาวพร้อมข้อความ "ลากฉันเพื่อปิด" ซึ่งผู้ใช้สามารถปัดไปทางซ้ายเพื่อปิด (หรือลากไปทางขวาเพื่อดูเอฟเฟกต์การยืดแบบเลื่อนเกิน)
ตัวอย่าง: สร้างลิ้นชักการนำทางที่กำหนดเองซึ่งทำงานด้วยท่าทางสัมผัส
ตัวอย่างนี้สาธิตวิธีสร้างลิ้นชักการนำทางที่กำหนดเองซึ่งทำงานด้วยท่าทางสัมผัส เมื่อผู้ใช้ลากในแนวนอน หน้าจอที่อยู่เบื้องหน้า (ScreenContents) จะเลื่อนไปทางขวา ลดขนาดจาก 1.0f เป็น 0.8f และปรับมุมให้โค้งมนเพื่อแสดงลิ้นชักการนำทางที่อยู่ด้านล่าง
กำหนดสถานะและตำแหน่งของโฆษณา Anchor ในตัวอย่างนี้ Anchor คือสถานะเปิดและปิดของลิ้นชัก กำหนด 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, ) }
ทำให้
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(...) } }
ใช้
state.requireOffset()เพื่อเปลี่ยนพร็อพเพอร์ตี้ภาพหลายรายการ คุณสามารถเปลี่ยนพร็อพเพอร์ตี้หลายรายการด้วยModifier.graphicsLayerเพื่อให้ภาพเคลื่อนไหวของการแปล ขนาด และรัศมีมุมทำงานพร้อมกันตามความคืบหน้าของการลาก แทนที่จะชดเชยเนื้อหาเพียงอย่างเดียว- ใช้
state.requireOffset()เพื่อรับออฟเซ็ตการลากปัจจุบัน - ใช้
lerp()(การประมาณค่าในช่วงเชิงเส้น) เพื่อคำนวณสัดส่วน (1fถึง0.8f) และรัศมีมุม (0.dpถึง32.dp) ที่สัมพันธ์กับออฟเซ็ตและความกว้างของลิ้นชัก 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) ) }
- ใช้
ทริกเกอร์ภาพเคลื่อนไหวแบบเป็นโปรแกรมด้วย
animateToควบคุมลิ้นชัก แบบเป็นโปรแกรม (เช่น เมื่อแตะไอคอนเมนูหรือเลือกรายการลิ้นชัก ) โดยเปิดstate.animateTo()ภายในขอบเขต Coroutinefun toggleDrawerState( state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { coroutineScope.launch { if (state.currentValue == DrawerState.Open) { state.animateTo(DrawerState.Closed) } else { state.animateTo(DrawerState.Open) } } }
ผลลัพธ์
ลากใน 2 มิติด้วย Modifier.draggable2D
ใช้ตัวปรับแต่ง draggable2D เพื่อย้ายองค์ประกอบ UI แต่ละรายการทั้งในแกน X และ Y พร้อมกัน
ลากองค์ประกอบที่ประกอบกันได้
ตัวอย่างนี้แสดงกรณีการใช้งานที่พบบ่อยที่สุดสำหรับ 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) } } }
ข้อมูลโค้ดก่อนหน้าประกอบด้วยข้อมูลต่อไปนี้
- ติดตามตำแหน่งของกล่องโดยใช้สถานะ
offset - ใช้ตัวแก้ไข
offsetเพื่อเปลี่ยนตำแหน่งของคอมโพเนนต์ตามเดลต้าการลาก - เนื่องจากไม่มีการรองรับการเหวี่ยง กล่องจึงหยุดเคลื่อนที่ทันทีที่ผู้ใช้ยกนิ้วขึ้น
ลาก Composable ย่อยตามพื้นที่การลากของ Composable หลัก
ตัวอย่างนี้แสดงวิธีใช้ 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) } ) ) } }
ข้อมูลโค้ดก่อนหน้าประกอบด้วยข้อมูลต่อไปนี้
- โดยใช้ตัวแก้ไข
onSizeChangedเพื่อบันทึกขนาดจริงของคอนเทนเนอร์การไล่ระดับสี ตัวเลือกจะทราบตำแหน่งของขอบอย่างแม่นยำ - ภายใน
graphicsLayerจะปรับtranslationXและtranslationYเพื่อให้ตัวเลือกอยู่ตรงกลางขณะลาก
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- ทำความเข้าใจท่าทางสัมผัส
- ตัวอย่างภาพเคลื่อนไหวขั้นสูง: ท่าทางสัมผัส {:#gesture-and-animation}
- ภาพเคลื่อนไหวตามมูลค่า