Skip to Content

Gestures trong Jetpack Compose

Xử lý touch input và gestures là một phần quan trọng của mobile UI. Compose cung cấp APIs trực quan để detect và phản hồi các gestures.

1. Click và Tap cơ bản

clickable modifier

@Composable fun ClickableBox() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .clickable { println("Clicked!") } ) }

clickable với parameters

@Composable fun AccessibleButton() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .clickable( enabled = true, onClick = { }, role = Role.Button, onClickLabel = "Perform action" // For accessibility ) ) }

combinedClickable

@Composable fun MultiClickBox() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .combinedClickable( onClick = { println("Single click") }, onDoubleClick = { println("Double click") }, onLongClick = { println("Long press") } ) ) }

2. Pointer Input - Low-level API

detectTapGestures

@Composable fun TapGesturesBox() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .pointerInput(Unit) { detectTapGestures( onTap = { offset -> println("Tap at $offset") }, onDoubleTap = { offset -> println("Double tap at $offset") }, onLongPress = { offset -> println("Long press at $offset") }, onPress = { offset -> // Called immediately when pressed val released = tryAwaitRelease() if (released) { println("Released") } else { println("Cancelled") } } ) } ) }

3. Scroll

verticalScroll / horizontalScroll

@Composable fun ScrollableContent() { val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) ) { repeat(50) { index -> Text("Item $index", modifier = Modifier.padding(16.dp)) } } }

Programmatic scrolling

@Composable fun ControlledScroll() { val scrollState = rememberScrollState() val scope = rememberCoroutineScope() Column { Button(onClick = { scope.launch { scrollState.animateScrollTo(0) // Scroll to top } }) { Text("Scroll to Top") } Column(modifier = Modifier.verticalScroll(scrollState)) { // Content } } }

Observe scroll position

@Composable fun ScrollObserver() { val scrollState = rememberScrollState() LaunchedEffect(scrollState) { snapshotFlow { scrollState.value } .collect { scrollPosition -> println("Scroll position: $scrollPosition") } } Column(modifier = Modifier.verticalScroll(scrollState)) { // Content } }

4. Drag

draggable modifier

@Composable fun DraggableBox() { var offsetX by remember { mutableStateOf(0f) } Box( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .size(100.dp) .background(Color.Blue) .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> offsetX += delta } ) ) }

detectDragGestures - 2D drag

@Composable fun FreeDraggableBox() { var offset by remember { mutableStateOf(Offset.Zero) } Box( modifier = Modifier .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } .size(100.dp) .background(Color.Blue) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() offset += dragAmount } } ) }

Drag với animation

@Composable fun SnapBackDraggable() { var offset by remember { mutableStateOf(Offset.Zero) } val animatedOffset by animateOffsetAsState( targetValue = offset, animationSpec = spring(stiffness = Spring.StiffnessLow), label = "offset" ) Box( modifier = Modifier .offset { IntOffset(animatedOffset.x.roundToInt(), animatedOffset.y.roundToInt()) } .size(100.dp) .background(Color.Blue) .pointerInput(Unit) { detectDragGestures( onDragEnd = { offset = Offset.Zero // Snap back } ) { change, dragAmount -> change.consume() offset += dragAmount } } ) }

5. Swipe

Swipe to Dismiss

@OptIn(ExperimentalMaterial3Api::class) @Composable fun SwipeItem(onDismiss: () -> Unit) { val dismissState = rememberSwipeToDismissBoxState( confirmValueChange = { value -> if (value == SwipeToDismissBoxValue.EndToStart) { onDismiss() true } else { false } } ) SwipeToDismissBox( state = dismissState, backgroundContent = { Box( modifier = Modifier .fillMaxSize() .background(Color.Red), contentAlignment = Alignment.CenterEnd ) { Icon( Icons.Default.Delete, null, modifier = Modifier.padding(16.dp), tint = Color.White ) } } ) { Card { Text("Swipe to delete", modifier = Modifier.padding(16.dp)) } } }

Custom Swipe Detection

@Composable fun CustomSwipe() { var offsetX by remember { mutableStateOf(0f) } var swiped by remember { mutableStateOf(false) } val animatedOffset by animateFloatAsState( targetValue = if (swiped) 500f else offsetX, label = "swipe" ) Box( modifier = Modifier .offset { IntOffset(animatedOffset.roundToInt(), 0) } .fillMaxWidth() .height(60.dp) .background(Color.Blue) .pointerInput(Unit) { detectHorizontalDragGestures( onDragEnd = { if (abs(offsetX) > 200) { swiped = true } else { offsetX = 0f } } ) { change, dragAmount -> change.consume() offsetX += dragAmount } } ) { Text("Swipe me", color = Color.White) } }

6. Transform Gestures - Zoom, Rotate, Pan

@Composable fun TransformableImage() { var scale by remember { mutableStateOf(1f) } var rotation by remember { mutableStateOf(0f) } var offset by remember { mutableStateOf(Offset.Zero) } val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> scale *= zoomChange rotation += rotationChange offset += offsetChange } Image( painter = painterResource(R.drawable.image), contentDescription = null, modifier = Modifier .graphicsLayer( scaleX = scale, scaleY = scale, rotationZ = rotation, translationX = offset.x, translationY = offset.y ) .transformable(state = state) .fillMaxSize() ) }

detectTransformGestures (low-level)

@Composable fun ManualTransform() { var scale by remember { mutableStateOf(1f) } var rotation by remember { mutableStateOf(0f) } var offset by remember { mutableStateOf(Offset.Zero) } Box( modifier = Modifier .graphicsLayer( scaleX = scale, scaleY = scale, rotationZ = rotation, translationX = offset.x, translationY = offset.y ) .pointerInput(Unit) { detectTransformGestures { centroid, pan, zoom, rotationDelta -> scale *= zoom rotation += rotationDelta offset += pan } } .size(200.dp) .background(Color.Blue) ) }

7. Nested Scrolling

nestedScroll modifier

@OptIn(ExperimentalMaterial3Api::class) @Composable fun CollapsibleToolbar() { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { LargeTopAppBar( title = { Text("Title") }, scrollBehavior = scrollBehavior ) } ) { padding -> LazyColumn(modifier = Modifier.padding(padding)) { items(100) { index -> Text("Item $index", modifier = Modifier.padding(16.dp)) } } } }

8. Accessibility và Gestures

@Composable fun AccessibleGestures() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .semantics { onClick(label = "Perform main action") { // Handle click for accessibility true } onLongClick(label = "Show menu") { // Handle long click true } } .pointerInput(Unit) { detectTapGestures( onTap = { /* Regular tap */ }, onLongPress = { /* Regular long press */ } ) } ) }

9. Custom Gesture Detector

suspend fun PointerInputScope.detectTripleTap( onTripleTap: (Offset) -> Unit ) { awaitEachGesture { val firstDown = awaitFirstDown() val firstUp = waitForUpOrCancellation() if (firstUp == null || (firstUp.uptimeMillis - firstDown.uptimeMillis) > 200) return@awaitEachGesture val secondDown = withTimeoutOrNull(300) { awaitFirstDown() } if (secondDown == null) return@awaitEachGesture val secondUp = waitForUpOrCancellation() if (secondUp == null) return@awaitEachGesture val thirdDown = withTimeoutOrNull(300) { awaitFirstDown() } if (thirdDown == null) return@awaitEachGesture val thirdUp = waitForUpOrCancellation() if (thirdUp != null) { onTripleTap(thirdUp.position) } } } @Composable fun TripleTapBox() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .pointerInput(Unit) { detectTripleTap { offset -> println("Triple tap at $offset") } } ) }

📝 Tóm tắt

GestureAPI
Tapclickable, detectTapGestures
Long PresscombinedClickable, detectTapGestures
ScrollverticalScroll, horizontalScroll
Dragdraggable, detectDragGestures
SwipeSwipeToDismissBox, custom
Transformtransformable, detectTransformGestures

Chọn API nào?

Cần đơn giản? ├── Click → clickable ├── Multiple click types → combinedClickable └── Scroll → verticalScroll/horizontalScroll Cần customize? └── pointerInput + detect*Gestures
Last updated on