From 608e88b24dc5cc76651b858d5161124fdd0ac308 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Wed, 1 Dec 2021 13:28:09 +0300 Subject: [PATCH] New tutorial for events (#1482) --- tutorials/Mouse_Events/README.md | 182 +++++++++++++++++++++++++------ 1 file changed, 146 insertions(+), 36 deletions(-) diff --git a/tutorials/Mouse_Events/README.md b/tutorials/Mouse_Events/README.md index 34c78a746b..7fa4272c4e 100644 --- a/tutorials/Mouse_Events/README.md +++ b/tutorials/Mouse_Events/README.md @@ -9,8 +9,7 @@ in Compose for Desktop. ### Click listeners -Click listeners are available in both Compose on Android and Compose for Desktop, -so code like this will work on both platforms: +Click listeners are available in both Compose on Android and Compose for Desktop, so code like this will work on both platforms: ```kotlin import androidx.compose.foundation.ExperimentalFoundationApi @@ -64,10 +63,7 @@ fun main() = singleWindowApplication { ### Mouse move listeners -As typically mouse and other positional pointers are only available on desktop platforms, -the following code will only work with Compose for Desktop. -Let's create a window and install a pointer move filter on it that changes the background -color according to the mouse pointer position: +Let's create a window and install a pointer move listener on it that changes the background color according to the mouse pointer position: ```kotlin import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -81,7 +77,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerMoveFilter +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.window.singleWindowApplication @OptIn(ExperimentalComposeUiApi::class) @@ -92,15 +89,14 @@ fun main() = singleWindowApplication { .wrapContentSize(Alignment.Center) .fillMaxSize() .background(color = color) - .pointerMoveFilter( - onMove = { - color = Color(it.x.toInt() % 256, it.y.toInt() % 256, 0) - false - } - ) + .onPointerEvent(PointerEventType.Move) { + val position = it.changes.first().position + color = Color(position.x.toInt() % 256, position.y.toInt() % 256, 0) + } ) } ``` +*Note that onPointerEvent is experimental and can be changed in the future. For more stable API look at [Modifier.pointerInput](#listenining-raw-events-in-commonmain-via-modifierpointerinput)*. Application running @@ -120,7 +116,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerMoveFilter +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -138,16 +135,8 @@ fun main() = singleWindowApplication { modifier = Modifier .fillMaxWidth() .background(color = if (active) Color.Green else Color.White) - .pointerMoveFilter( - onEnter = { - active = true - false - }, - onExit = { - active = false - false - } - ), + .onPointerEvent(PointerEventType.Enter) { active = true } + .onPointerEvent(PointerEventType.Exit) { active = false }, fontSize = 30.sp, fontStyle = if (active) FontStyle.Italic else FontStyle.Normal, text = "Item $index" @@ -156,8 +145,44 @@ fun main() = singleWindowApplication { } } ``` +*Note that onPointerEvent is experimental and can be changed in the future. For more stable API look at [Modifier.pointerInput](#listenining-raw-events-in-commonmain-via-modifierpointerinput)*. + Application running +### Mouse scroll listeners +```kotlin +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.singleWindowApplication + +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication { + var number by remember { mutableStateOf(0f) } + Box( + Modifier + .fillMaxSize() + .onPointerEvent(PointerEventType.Scroll) { + number += it.changes.first().scrollDelta.y + }, + contentAlignment = Alignment.Center + ) { + Text("Scroll to change the number: $number", fontSize = 30.sp) + } +} +``` +*Note that onPointerEvent is experimental and can be changed in the future. For more stable API look at [Modifier.pointerInput](#listenining-raw-events-in-commonmain-via-modifierpointerinput)*. + ### Mouse right/middle clicks and keyboard modifiers Compose for Desktop contains desktop-only `Modifier.mouseClickable`, where data about pressed mouse buttons and keyboard modifiers is available. This is an experimental API, which means that it's likely to be changed before release. @@ -208,11 +233,54 @@ fun main() = singleWindowApplication { ``` Application running +If you need to listen left/right clicks simultaneously, you should listen for raw events: +``` +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.input.pointer.isSecondaryPressed +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.singleWindowApplication + +@OptIn(ExperimentalComposeUiApi::class) +fun main() = singleWindowApplication { + var text by remember { mutableStateOf("Press me") } + + Box( + Modifier + .fillMaxSize() + .onPointerEvent(PointerEventType.Press) { + val position = it.changes.first().position + text = when { + it.buttons.isPrimaryPressed && + it.buttons.isSecondaryPressed -> "Left+Right click $position" + it.buttons.isSecondaryPressed -> "Right click $position" + it.buttons.isPrimaryPressed -> "Left click $position" + else -> text + } + }, + contentAlignment = Alignment.Center + ) { + Text(text, fontSize = 30.sp) + } +} +``` +*Note that onPointerEvent is experimental and can be changed in the future. For more stable API look at [Modifier.pointerInput](#listenining-raw-events-in-commonmain-via-modifierpointerinput)*. + ### Swing interoperability Compose for Desktop uses Swing underneath and allows to access raw AWT events: - -```kotlin +``` import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text @@ -221,26 +289,68 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.awt.awtEvent +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.window.singleWindowApplication +@OptIn(ExperimentalComposeUiApi::class) fun main() = singleWindowApplication { var text by remember { mutableStateOf("") } Box( - Modifier.fillMaxSize().pointerInput(Unit) { - while (true) { - val event = awaitPointerEventScope { awaitPointerEvent() } - val awtEvent = event.mouseEvent - if (event.type == PointerEventType.Press) { - text = awtEvent?.locationOnScreen?.toString().orEmpty() - } - } - }, + Modifier + .fillMaxSize() + .onPointerEvent(PointerEventType.Press) { + text = it.awtEvent.locationOnScreen?.toString().orEmpty() + }, contentAlignment = Alignment.Center ) { Text(text) } } ``` +*Note that onPointerEvent is experimental and can be changed in the future. For more stable API look at [Modifier.pointerInput](#listenining-raw-events-in-commonmain-via-modifierpointerinput)*. + +### Listenining raw events in commonMain via Modifier.pointerInput +In the snippets above we use `Modifier.onPointerEvent`, which is a helper function to subscribe to some type of pointer events. It is a shorter variant of `Modifier.pointerInput`. For now it is experimental, and desktop-only (you can't use it in commonMain code). If you need to subscribe to events in commonMain or you need stable API, you can use `Modifier.pointerInput`: + +```kotlin +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.window.singleWindowApplication + +fun main() = singleWindowApplication { + val list = remember { mutableStateListOf() } + + Column( + Modifier + .fillMaxSize() + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + val position = event.changes.first().position + // on every relayout Compose will send synthetic Move event, + // so we skip it to avoid event spam + if (event.type != PointerEventType.Move) { + list.add(0, "${event.type} $position") + } + } + } + }, + ) { + for (item in list.take(20)) { + Text(item) + } + } +} +```