Việc cho phép người dùng kéo các thành phần trên màn hình là điều cơ bản đối với các ứng dụng Android hiện đại. Cho dù bạn đang tạo thanh trượt tuỳ chỉnh, thành phần vuốt để đóng hay canvas tương tác, Jetpack Compose đều cung cấp một bộ API toàn diện để dễ dàng xử lý các sự kiện kéo.
Các API kéo trong Compose giúp bạn không phải lo lắng về độ phức tạp của các sự kiện chạm thô.
Thay vì tính toán toạ độ chạm theo cách thủ công và quản lý vòng đời cử chỉ bằng pointerInput, bạn có thể sử dụng các đối tượng sửa đổi tích hợp liên kết trực tiếp với hệ thống quản lý trạng thái và ảnh động của Compose.
Trang này mô tả cách và thời điểm sử dụng từng API kéo.
Chọn API kéo phù hợp
Bạn có thể sử dụng một số API để triển khai nội dung có thể kéo.
- Chọn các thành phần Material tiêu chuẩn (như
SwipeToDismissBoxhoặc ngăn điều hướng) khi: Bạn cần hành vi vuốt hoặc kéo Material 3 mặc định (ví dụ: xoá email, lưu trữ tác vụ hoặc mở trình đơn bên / bảng dưới cùng). Các thành phần này cung cấp hành vi tích hợp mà không yêu cầu trạng thái hoặc quản lý bố cục tuỳ chỉnh. - Chọn
Modifier.draggablekhi: Bạn chỉ cần các delta kéo thô (ví dụ: kéo thanh trượt tuỳ chỉnh). Để biết thêm thông tin, hãy xem Kéo dọc theo một trục bằng đối tượng sửa đổi có thể kéo.- Chọn
Modifier.draggable2Dkhi: Bạn cần một thành phần di chuyển liên tục trên cả trục X và Y cùng một lúc (ví dụ: di chuyển trên bản đồ hoặc tạo nút hành động nổi mà người dùng có thể kéo đến bất kỳ vị trí nào trên màn hình). Để biết thêm thông tin, hãy xem Kéo theo hai chiều bằngModifier.draggable2D.
- Chọn
- Chọn
Modifier.anchoredDraggablekhi: Bạn đang tạo các thành phần tương tác tuỳ chỉnh cần phải khớp giữa 2 hoặc nhiều vị trí riêng biệt (ví dụ: bảng dưới cùng, ngăn điều hướng hoặc hành động vuốt để hiện). Để biết thêm thông tin, hãy xem Kéo các thành phần giữa các điểm neo bằnganchoredDraggable.
Kéo dọc theo một trục bằng draggable
Đối tượng sửa đổi draggable là điểm truy cập cấp cao cho các cử chỉ kéo theo
một hướng đồng thời báo cáo khoảng cách kéo bằng pixel.
Quan trọng là bạn phải lưu ý rằng đối tượng sửa đổi này tương tự như scrollable, vì đối tượng này chỉ phát hiện cử chỉ. Bạn cần giữ và biểu thị trạng thái trên màn hình
bằng cách di chuyển phần tử qua offset đối tượng sửa đổi:
@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!" ) }
Nếu bạn cần điều khiển toàn bộ cử chỉ kéo, hãy cân nhắc chuyển sang sử dụng trình phát hiện cử chỉ kéo, thông qua đối tượng sửa đổi 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 } } ) } }
Kéo các thành phần giữa các điểm neo bằng anchoredDraggable
Đối tượng sửa đổi anchoredDraggable cho phép bạn kéo một thành phần giữa một tập hợp các
vị trí được xác định trước gọi là điểm neo. Khi người dùng nhả cử chỉ kéo, thành phần sẽ dừng ở điểm neo gần nhất. anchoredDraggable rất hữu ích để tạo các thành phần như bảng dưới cùng tuỳ chỉnh, thanh trượt phức tạp hoặc bảng điều khiển ngăn.
Bề mặt API
API AnchoredDraggable bao gồm các phần chính sau:
- Điểm neo (
DraggableAnchors): Trình tạo xác định mối liên kết giữa các trạng thái tuỳ chỉnh (ví dụ: lớp enum choStart, Center, End) và các giá trị bù pixel tương ứng. - Trạng thái (
AnchoredDraggableState<T>): Phần tử giữ trạng thái quản lý vị trí hiện tại (bù) của thành phần, điểm neo mà thành phần dừng ở đó và tiến trình kéo của thành phần. Bạn nên tạo trạng thái này bằngrememberSaveablevàAnchoredDraggableState.Saver<T>để giữ nguyên trạng thái của thành phần khi có những thay đổi về cấu hình. - Đối tượng sửa đổi cử chỉ (
Modifier.anchoredDraggable): Đối tượng sửa đổi phát hiện các cử chỉ kéo dọc theoOrientation, cập nhật trạng thái bằng delta kéo và xử lý cử chỉ vuốt để tạo ảnh động cho thành phần đến điểm neo gần nhất khi nhả.
Ví dụ: Tạo thành phần SwipeToDismiss tuỳ chỉnh
Ví dụ này minh hoạ cách tạo thành phần SwipeToDismiss tuỳ chỉnh (biểu ngữ màu trắng) mà người dùng có thể vuốt sang trái để đóng hoặc kéo sang phải để xem hiệu ứng kéo giãn khi cuộn quá mức.
Xác định giá trị điểm neo. Xác định một enum đại diện cho các vị trí riêng biệt mà nội dung kéo được có thể dừng ở đó:
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
Tạo thực thể và lưu
AnchoredDraggableState. Sử dụngrememberSaveablevớiAnchoredDraggableState.Saver()để duy trì vị trí hiện tại của thành phần khi tạo lại hoạt động và thay đổi cấu hình (ví dụ: xoay màn hình hoặc thay đổi giao diện):val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting) }
Áp dụng các đối tượng sửa đổi
layout,offsetvàdraggable. Để tạo thành phần có thể kéo được và đảm bảo các điểm neo của thành phần thích ứng với kích thước của thành phần, hãy áp dụng các modifier sau đây cho nội dung cần di chuyển trực quan:Modifier.layout: Vị trí điểm neo thường phụ thuộc vào kích thước của thành phần. Kích thước này không được biết trong quá trình tạo ban đầu. Sử dụng đối tượng sửa đổilayoutđể đo lường thành phần, tính toán động bản đồDraggableAnchorsdựa trên chiều rộng của thành phần và cập nhật trạng thái bằngstate.updateAnchors().
Modifier.anchoredDraggable: Kết nối với trạng thái, gửi các delta kéo theo hướng đã chỉ định và xử lý cử chỉ vuốt để tạo ảnh động cho thành phần đến điểm neo gần nhất khi nhả. Thêm đối tượng sửa đổi này vào thành phần cần phản hồi các cử chỉ kéo và truyền cho đối tượng nàystatevà kéoorientation(và tuỳ ýoverscrollEffect).Modifier.offset: Đọc độ bù từstate.requireOffset()bên trong lambda bù. Việc đọc độ bù trong lambda này đảm bảo giá trị chỉ được đọc trong giai đoạn đặt, tránh các lần tạo lại không cần thiết trong quá trình kéo. Áp dụng đối tượng sửa đổi này cho nội dung cần di chuyển trực quan.
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() }
[Không bắt buộc] Tích hợp tính năng cuộn quá mức. API
overscrollEffecttriển khai một hiệu ứng hình ảnh xuất hiện khi các cạnh của vùng chứa cuộn đã đạt đến một lần cuộn hoặc vuốt. Bạn có thể cung cấp ý kiến phản hồi trực quan khi người dùng kéo qua các giới hạn điểm neo đã xác định bằng hiệu ứng cuộn quá mức (thường là hiệu ứng kéo giãn hoặc phát sáng).- Gửi: Truyền thực thể
overscrollEffectvào đối tượng sửa đổianchoredDraggable. Thao tác này sẽ gửi các delta kéo đến stateAnchoredDraggablehoặc hiệu ứng cuộn quá mức, tuỳ thuộc vào việc cử chỉ nằm trong hay ngoài giới hạn điểm neo. - Kết xuất: Áp dụng
Modifier.overscrollvới cùng mộtOverscrollEffectthực thể cho thành phần cần kết xuất hiệu ứng một cách trực quan.
- Gửi: Truyền thực thể
Đoạn mã sau đây cho biết cách thêm hiệu ứng kéo giãn tiêu chuẩn:
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() } }
Kết quả
Đoạn mã trước đó tạo ra một biểu ngữ màu trắng, với văn bản "Kéo tôi để đóng!", mà người dùng có thể vuốt sang trái để đóng (hoặc kéo sang phải để xem hiệu ứng kéo giãn quá mức):
Ví dụ: Tạo ngăn điều hướng tuỳ chỉnh, dựa trên cử chỉ
Ví dụ này minh hoạ cách tạo ngăn điều hướng tuỳ chỉnh, dựa trên cử chỉ. Khi người dùng kéo theo chiều ngang, màn hình nền trước (ScreenContents) sẽ trượt sang phải, thu nhỏ từ 1.0f xuống 0.8f và bo tròn các góc để hiện ngăn điều hướng bên dưới.
Xác định trạng thái và vị trí điểm neo. Trong ví dụ này, các điểm neo là trạng thái mở và đóng của ngăn. Xác định một enum cho trạng thái
OpenvàClosed, đồng thời liên kết các trạng thái này với độ bù pixel bằngDraggableAnchors: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, ) }
Tạo ảnh động cho
anchoredDraggableStatekhi nhấp vào một mục. ĐặtHomeScreenDrawerở cuốiBoxvàScreenContentsở trên cùng. Ngăn vẫn đứng yên, trong khi lớp trên cùng dịch và mở rộng trên ngăn.Khi người dùng chọn một mục trong ngăn, hãy cập nhật trạng thái màn hình đang hoạt động và tự động đóng ngăn bằng
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(...) } }
Sử dụng
state.requireOffset()để thay đổi nhiều thuộc tính trực quan. Thay vì chỉ bù nội dung, bạn có thể thay đổi nhiều thuộc tính bằngModifier.graphicsLayerđể đồng thời tạo ảnh động cho quá trình dịch, mở rộng và bán kính góc dựa trên tiến trình kéo:- Sử dụng
state.requireOffset()để lấy độ bù kéo hiện tại. - Sử dụng
lerp()(nội suy tuyến tính) để tính toán tỷ lệ tương ứng (1fđến0.8f) và bán kính góc (0.dpđến32.dp) so với độ bù và chiều rộng ngăn. Modifier.graphicsLayerthực thi các thay đổi trong giai đoạn vẽ, đảm bảo ảnh động hiệu suất cao mà không kích hoạt quá trình tạo lại trong quá trình kéo.
@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) ) }
- Sử dụng
Kích hoạt ảnh động có lập trình bằng
animateTo. Điều khiển ngăn theo cách lập trình (chẳng hạn như khi nhấn vào biểu tượng trình đơn hoặc chọn một mục trong ngăn) bằng cách khởi chạystate.animateTo()trong phạm vi coroutine:fun toggleDrawerState( state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { coroutineScope.launch { if (state.currentValue == DrawerState.Open) { state.animateTo(DrawerState.Closed) } else { state.animateTo(DrawerState.Open) } } }
Kết quả
Kéo theo hai chiều bằng Modifier.draggable2D
Sử dụng đối tượng sửa đổi draggable2D để di chuyển các phần tử trên giao diện người dùng riêng lẻ trên cả trục X và Y cùng một lúc.
Kéo một thành phần có thể kết hợp
Ví dụ này cho thấy trường hợp sử dụng phổ biến nhất cho draggable2D – cho phép người dùng chọn một phần tử trên giao diện người dùng và định vị lại phần tử đó ở bất kỳ vị trí nào trong vùng chứa mẹ.
@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) } } }
Đoạn mã trước đó bao gồm những nội dung sau:
- Theo dõi vị trí của hộp bằng trạng thái
offset. - Sử dụng đối tượng sửa đổi
offsetđể chuyển vị trí của thành phần dựa trên các delta kéo. - Vì không có hỗ trợ vuốt, nên hộp sẽ ngừng di chuyển ngay khi người dùng nhấc ngón tay lên.
Kéo một thành phần con có thể kết hợp dựa trên vùng kéo của thành phần mẹ
Ví dụ này minh hoạ cách sử dụng draggable2D để tạo vùng nhập 2D, trong đó núm bộ chọn bị giới hạn trong một bề mặt cụ thể. Không giống như ví dụ về thành phần có thể kéo, di chuyển chính thành phần đó, cách triển khai này sử dụng các delta 2D để di chuyển "bộ chọn" có thể kết hợp con trên bộ chọn màu:
@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) } ) ) } }
Đoạn mã trước đó bao gồm những nội dung sau:
- Đoạn mã này sử dụng đối tượng sửa đổi
onSizeChangedđể ghi lại kích thước thực tế của vùng chứa chuyển màu. Bộ chọn biết chính xác vị trí của các cạnh. - Bên trong
graphicsLayer, đoạn mã này điều chỉnhtranslationXvàtranslationYđể đảm bảo bộ chọn luôn ở giữa trong khi kéo.
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Tìm hiểu về cử chỉ
- Ví dụ về ảnh động nâng cao: Cử chỉ {:#gesture-and-animation}
- Ảnh động dựa trên giá trị