การอนุญาตให้ผู้ใช้ลากคอมโพเนนต์ข้ามหน้าจอด้วยตนเองเป็นสิ่งสำคัญสำหรับแอป 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 } } ) } }
การลากคอมโพเนนต์ระหว่างจุดยึดด้วย 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 ที่กำหนดเอง (แบนเนอร์สีขาว) ที่ผู้ใช้ปัดไปทางซ้ายเพื่อปิด หรือลากไปทางขวาเพื่อดูเอฟเฟกต์การยืดเมื่อเลื่อนเลย
กำหนดค่าจุดยึด กำหนด 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: ตำแหน่งจุดยึดมักจะขึ้นอยู่กับขนาดของคอมโพเนนต์ ซึ่งไม่ทราบในระหว่างการจัดองค์ประกอบเริ่มต้น ใช้ตัวปรับแต่ง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() }
[ไม่บังคับ] ผสานรวมการเลื่อนเกิน API
overscrollEffectจะติดตั้งใช้งานเอฟเฟกต์ภาพที่แสดงขึ้นเมื่อมีการเลื่อนหรือปัดไปถึงขอบของคอนเทนเนอร์ที่เลื่อนได้ คุณสามารถแสดงความคิดเห็นด้วยภาพเมื่อผู้ใช้ลากเลยขอบเขตจุดยึดที่กำหนดไว้ด้วยเอฟเฟกต์การเลื่อนเลย (โดยปกติจะเป็นการยืดหรือเรืองแสง)- ส่ง: ส่งอินสแตนซ์
overscrollEffectไปยังตัวปรับแต่งanchoredDraggableซึ่งจะส่งค่าเดลต้าการลากไปยังสถานะAnchoredDraggableหรือเอฟเฟกต์การเลื่อนเกิน ทั้งนี้ขึ้นอยู่กับว่าท่าทางสัมผัสอยู่ภายในหรือภายนอกขอบเขตจุดยึด - แสดงผล: ใช้
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() } }
ผลลัพธ์
ข้อมูลโค้ดก่อนหน้าจะแสดงแบนเนอร์สีขาวที่มีข้อความ "ลากฉันเพื่อปิด!" ซึ่งผู้ใช้ปัดไปทางซ้ายเพื่อปิด (หรือลากไปทางขวาเพื่อดูเอฟเฟกต์การยืดเมื่อเลื่อนเลย) ได้
ตัวอย่าง: สร้างลิ้นชักการนำทางที่กำหนดเองซึ่งขับเคลื่อนด้วยท่าทางสัมผัส
ตัวอย่างนี้แสดงวิธีสร้าง Navigation Drawer ที่กำหนดเองซึ่งขับเคลื่อนด้วยท่าทางสัมผัส เมื่อผู้ใช้ลากในแนวนอน หน้าจอเบื้องหน้า (ScreenContents) จะเลื่อนไปทางขวา ลดขนาดจาก 1.0f เป็น 0.8f และปัดมุมเพื่อแสดง Navigation Drawer ที่อยู่ด้านล่าง
กำหนดสถานะและตำแหน่งจุดยึด ในตัวอย่างนี้ จุดยึดคือสถานะเปิดและปิดของ 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, ) }
แสดงภาพเคลื่อนไหว
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(...) } }
ใช้
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) ) }
- ใช้
ทริกเกอร์ภาพเคลื่อนไหวแบบเป็นโปรแกรมด้วย
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) } } }
ผลลัพธ์
การลากใน 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) } } }
ข้อมูลโค้ดก่อนหน้ามีข้อมูลต่อไปนี้
- ติดตามตำแหน่งของกล่องโดยใช้สถานะ
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) } ) ) } }
ข้อมูลโค้ดก่อนหน้ามีข้อมูลต่อไปนี้
- ใช้ตัวปรับแต่ง
onSizeChangedเพื่อบันทึกขนาดจริงของคอนเทนเนอร์การไล่ระดับสี ตัวเลือกจะทราบตำแหน่งขอบที่แน่นอน - ภายใน
graphicsLayerตัวเลือกจะปรับtranslationXและtranslationYเพื่อให้แน่ใจว่าตัวเลือกจะอยู่ตรงกลางขณะลาก
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- ทำความเข้าใจท่าทางสัมผัส
- ตัวอย่างภาพเคลื่อนไหวขั้นสูง: ท่าทางสัมผัส {:#gesture-and-animation}
- ภาพเคลื่อนไหวตามมูลค่า