Tools: Jetpack Compose Gestures — Tap, Swipe, Drag Patterns

Tools: Jetpack Compose Gestures — Tap, Swipe, Drag Patterns

Source: Dev.to

Jetpack Compose Gestures — Tap, Swipe, Drag Patterns ## 1. Clickable — Simple Tap Detection ## 2. CombinedClickable — Long Press + Double Tap ## 3. SwipeToDismissBox — Swipe to Remove ## 4. Draggable — Single-Direction Movement ## 5. PointerInput + DetectDragGestures — Multi-Direction & Custom Logic ## 6. Pinch Zoom with DetectTransformGestures ## Pattern Selection Quick Reference ## Performance Tips ## Next Steps Building interactive Android apps requires responsive gesture handling. Jetpack Compose provides multiple APIs for different interaction patterns. Here's a practical guide. For basic button interactions: For custom clickable surfaces without button styling: Handle multiple tap patterns on the same element: Use cases: Context menus on long press, favorite toggle on double tap. Built-in support for dismissible list items: Constrain dragging to horizontal or vertical axis: Common use: Slider controls, horizontal scrollers. For complex drag patterns requiring offset tracking and custom constraints: Key pattern: change.consume() prevents propagation to parent composables. Multi-touch scaling, rotation, and pan detection: Use cases: Photo galleries, map viewers, design tools. Learn more about advanced pointer events and multi-gesture combination patterns in the official Compose documentation. Build interactive experiences that delight your users. 8 Android App Templates — Production-ready Kotlin + Compose apps ready to customize and ship to Google Play. → https://myougatheax.gumroad.com Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: Button( onClick = { /* handle click */ }, modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text("Tap Me") } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Button( onClick = { /* handle click */ }, modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text("Tap Me") } CODE_BLOCK: Button( onClick = { /* handle click */ }, modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text("Tap Me") } CODE_BLOCK: Text( "Click here", modifier = Modifier .clickable { /* action */ } .padding(16.dp) ) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Text( "Click here", modifier = Modifier .clickable { /* action */ } .padding(16.dp) ) CODE_BLOCK: Text( "Click here", modifier = Modifier .clickable { /* action */ } .padding(16.dp) ) CODE_BLOCK: Text( "Try long press or double tap", modifier = Modifier .combinedClickable( onClick = { println("Clicked") }, onDoubleClick = { println("Double tapped") }, onLongClick = { println("Long pressed") } ) .padding(16.dp) ) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Text( "Try long press or double tap", modifier = Modifier .combinedClickable( onClick = { println("Clicked") }, onDoubleClick = { println("Double tapped") }, onLongClick = { println("Long pressed") } ) .padding(16.dp) ) CODE_BLOCK: Text( "Try long press or double tap", modifier = Modifier .combinedClickable( onClick = { println("Clicked") }, onDoubleClick = { println("Double tapped") }, onLongClick = { println("Long pressed") } ) .padding(16.dp) ) CODE_BLOCK: var isDismissed by remember { mutableStateOf(false) } if (!isDismissed) { SwipeToDismissBox( modifier = Modifier.fillMaxWidth(), onDismissed = { isDismissed = true }, backgroundContent = { Box( Modifier .fillMaxSize() .background(Color.Red), contentAlignment = Alignment.CenterEnd ) { Icon(Icons.Default.Delete, contentDescription = null) } } ) { ListItem(headlineContent = { Text("Swipe to dismiss") }) } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: var isDismissed by remember { mutableStateOf(false) } if (!isDismissed) { SwipeToDismissBox( modifier = Modifier.fillMaxWidth(), onDismissed = { isDismissed = true }, backgroundContent = { Box( Modifier .fillMaxSize() .background(Color.Red), contentAlignment = Alignment.CenterEnd ) { Icon(Icons.Default.Delete, contentDescription = null) } } ) { ListItem(headlineContent = { Text("Swipe to dismiss") }) } } CODE_BLOCK: var isDismissed by remember { mutableStateOf(false) } if (!isDismissed) { SwipeToDismissBox( modifier = Modifier.fillMaxWidth(), onDismissed = { isDismissed = true }, backgroundContent = { Box( Modifier .fillMaxSize() .background(Color.Red), contentAlignment = Alignment.CenterEnd ) { Icon(Icons.Default.Delete, contentDescription = null) } } ) { ListItem(headlineContent = { Text("Swipe to dismiss") }) } } CODE_BLOCK: var offsetX by remember { mutableStateOf(0f) } Box( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( state = rememberDraggableState { delta -> offsetX += delta }, orientation = Orientation.Horizontal ) .size(100.dp) .background(Color.Blue) ) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: var offsetX by remember { mutableStateOf(0f) } Box( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( state = rememberDraggableState { delta -> offsetX += delta }, orientation = Orientation.Horizontal ) .size(100.dp) .background(Color.Blue) ) CODE_BLOCK: var offsetX by remember { mutableStateOf(0f) } Box( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( state = rememberDraggableState { delta -> offsetX += delta }, orientation = Orientation.Horizontal ) .size(100.dp) .background(Color.Blue) ) CODE_BLOCK: var position by remember { mutableStateOf(Offset(0f, 0f)) } Box( modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectDragGestures( onDrag = { change, offset -> change.consume() position += offset }, onDragEnd = { /* snap to grid, for example */ }, onDragCancel = { /* reset */ } ) } ) { Box( modifier = Modifier .offset { IntOffset(position.x.toInt(), position.y.toInt()) } .size(80.dp) .background(Color.Green) ) } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: var position by remember { mutableStateOf(Offset(0f, 0f)) } Box( modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectDragGestures( onDrag = { change, offset -> change.consume() position += offset }, onDragEnd = { /* snap to grid, for example */ }, onDragCancel = { /* reset */ } ) } ) { Box( modifier = Modifier .offset { IntOffset(position.x.toInt(), position.y.toInt()) } .size(80.dp) .background(Color.Green) ) } CODE_BLOCK: var position by remember { mutableStateOf(Offset(0f, 0f)) } Box( modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectDragGestures( onDrag = { change, offset -> change.consume() position += offset }, onDragEnd = { /* snap to grid, for example */ }, onDragCancel = { /* reset */ } ) } ) { Box( modifier = Modifier .offset { IntOffset(position.x.toInt(), position.y.toInt()) } .size(80.dp) .background(Color.Green) ) } CODE_BLOCK: var scale by remember { mutableStateOf(1f) } var offset by remember { mutableStateOf(Offset(0f, 0f)) } Image( painter = painterResource(id = R.drawable.sample), contentDescription = null, modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectTransformGestures { centroid, pan, zoom, rotation -> scale *= zoom offset += pan } } .graphicsLayer( scaleX = scale, scaleY = scale, translationX = offset.x, translationY = offset.y ) ) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: var scale by remember { mutableStateOf(1f) } var offset by remember { mutableStateOf(Offset(0f, 0f)) } Image( painter = painterResource(id = R.drawable.sample), contentDescription = null, modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectTransformGestures { centroid, pan, zoom, rotation -> scale *= zoom offset += pan } } .graphicsLayer( scaleX = scale, scaleY = scale, translationX = offset.x, translationY = offset.y ) ) CODE_BLOCK: var scale by remember { mutableStateOf(1f) } var offset by remember { mutableStateOf(Offset(0f, 0f)) } Image( painter = painterResource(id = R.drawable.sample), contentDescription = null, modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectTransformGestures { centroid, pan, zoom, rotation -> scale *= zoom offset += pan } } .graphicsLayer( scaleX = scale, scaleY = scale, translationX = offset.x, translationY = offset.y ) ) - Use rememberDraggableState to preserve state across recompositions - Call change.consume() in pointerInput to prevent gesture propagation - Avoid complex layout calculations during gesture callbacks - Test with ComposeTestRule and performTouchInput() for automated testing