Functions handling input events can be installed using methods on Modifier.
Managing clicks
Button has an argument onClick to register a click handler :
var counter by remember { mutableStateOf(0) } // in kg Button(onClick={ counter ++ }) { Text("Counter: $counter") }
Other components can deal with clicks using Modifier.clickable:
Modifier?.clickable( enabled: Boolean?, onClickLabel: String?, // text used for the accessibility role: Role?, onClick: (() -> Unit)? )
For example we can write a clickable image:
@Composable fun ClickableImage() { var opacity by remember { mutableStateOf(100) } Image(painter = painterResource(id = R.drawable.mercator), modifier = Modifier.clickable { opacity = (100 + opacity - 10) % 100 }, contentDescription = "world map", alignment = Alignment.Center, contentScale = ContentScale.Fit, alpha = opacity / 100.0f ) }
Touch events
- The View legacy API uses an OnTouchListener receiving MotionEvent objects to manage touch events
-
With compose two extension functions on Modifier are proposed to manage touch events:
- pointerInput
- pointerInteropFilter: provided only for interoperability to deal with legacy MotionEvent (otherwise pointerInput must be used)
Signature of pointerInput:
fun Modifier?.pointerInput( vararg keys: Any?, block: (@ExtensionFunctionType suspend PointerInputScope.() -> Unit)? ): Modifier
- keys are used to determine if the block coroutine must be cancelled and executed again (if any of the keys change after recomposition)
- block is a coroutine that is executed to handle touch events: one wait for events with calls to coroutines
Some useful coroutines to wait for events:
- awaitFirstDown(): waiting for the first finger to be put on the screen
-
val event = awaitPointerEvent(): wait for the next event
- event.changes contain all the change (with change.id for the id of the finger, change.position the position of the finger, change.pressed indicating if the finger is pressed)
- change.consumeAllChanges consume all the changes (they will not be given to other pointerInput coroutines)
- One can check if there remains at least one finger on the screen: val fingerRemaining = event.changes.any { it.pressed }
Example: an activity showing the mileage of fingers on the screen
@Composable fun FingerMileageDisplayer(fingerMileage: Map<Long, Float>, fingerPressed: Map<Long, Boolean>) { Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) { fingerMileage.forEach { entry -> Box( Modifier .fillMaxWidth() .background(if (fingerPressed[entry.key] == true) Color.Yellow else Color.LightGray)) { Text("${entry.value.toInt()}") } } } } @Composable fun PointerInputTest() { var fingerMileage = remember { mutableStateMapOf<Long, Float>() } var fingerPressed = remember { mutableStateMapOf<Long, Boolean>() } Box(modifier = Modifier .fillMaxSize() .background(Color.Green) .border(1.0.dp, Color.Black) .pointerInput(Unit) { awaitPointerEventScope { while (true) { awaitFirstDown() Log.v("InputEvent", "FirstDown") do { val event = awaitPointerEvent() event.changes.forEach { change -> fingerPressed[change.id.value] = change.pressed if (change.pressed) { val offset = change.position - change.previousPosition val distance = sqrt(offset.x * offset.x + offset.y * offset.y) fingerMileage.merge(change.id.value, distance) { x, y -> x + y } } change.consumeAllChanges() } val pressed = event.changes.any { it.pressed } } while (pressed) Log.v("InputEvent", "End of event") } } }) { FingerMileageDisplayer(fingerMileage, fingerPressed) } }
Rather than handling manually events, one can also use predefined detectors to make the task easier in PointerInputScope.
detectTapGestures allows the detection of taps on the screen:
suspend fun PointerInputScope?.detectTapGestures( onDoubleTap: ((Offset) -> Unit)? = null, onLongPress: ((Offset) -> Unit)? = null, onPress: (@ExtensionFunctionType suspend PressGestureScope.(Offset) -> Unit)? = NoPressGesture, onTap: ((Offset) -> Unit)? = null ): Unit
Complex gestures management
Compose proposes Modifier extension functions to manage complex gestures:
- Scrolling gesture with Modifier.scrollable(...)
- Dragging gesture with Modifier.draggable(...)
- Swiping gesture with Modifier.swipeable(...) with specified attractive anchors
- Multitouch gesture with Modifier.transformeable(...): one can pan (change the offset), zoom (change the scale) and rotate (change the rotation)
These functions do not modify the rendering for the component they are applied on. They are only used to update state variables (then one can modify rendering using these variables).
To update the state, special state handlers can be used:
- rememberScrollableState { delta -> ... } to scrolling
- rememberDraggableState { delta -> ... } for dragging
- rememberSwippeableState(position) for swiping
- rememberTransformState { zoomChange, offsetChange, rotationChange -> ... }