@ -1,247 +0,0 @@
|
||||
# Desktop components |
||||
|
||||
## What is covered |
||||
|
||||
In this tutorial, we will show you how to use desktop-specific components of Compose for Desktop such as scrollbars and tooltips. |
||||
|
||||
## Scrollbars |
||||
|
||||
You can apply scrollbars to scrollable components. The scrollbar and scrollable components share a common state to synchronize with each other. For example, `VerticalScrollbar` can be attached to `Modifier.verticalScroll`, and `LazyColumn` and `HorizontalScrollbar` can be attached to `Modifier.horizontalScroll` and `LazyRow`. |
||||
|
||||
```kotlin |
||||
import androidx.compose.foundation.HorizontalScrollbar |
||||
import androidx.compose.foundation.VerticalScrollbar |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.horizontalScroll |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxHeight |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.width |
||||
import androidx.compose.foundation.rememberScrollState |
||||
import androidx.compose.foundation.rememberScrollbarAdapter |
||||
import androidx.compose.foundation.verticalScroll |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.window.WindowState |
||||
import androidx.compose.ui.window.singleWindowApplication |
||||
|
||||
fun main() = singleWindowApplication( |
||||
title = "Scrollbars", |
||||
state = WindowState(width = 250.dp, height = 400.dp) |
||||
) { |
||||
Box( |
||||
modifier = Modifier.fillMaxSize() |
||||
.background(color = Color(180, 180, 180)) |
||||
.padding(10.dp) |
||||
) { |
||||
val stateVertical = rememberScrollState(0) |
||||
val stateHorizontal = rememberScrollState(0) |
||||
|
||||
Box( |
||||
modifier = Modifier |
||||
.fillMaxSize() |
||||
.verticalScroll(stateVertical) |
||||
.padding(end = 12.dp, bottom = 12.dp) |
||||
.horizontalScroll(stateHorizontal) |
||||
) { |
||||
Column { |
||||
for (item in 0..30) { |
||||
TextBox("Item #$item") |
||||
if (item < 30) { |
||||
Spacer(modifier = Modifier.height(5.dp)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
VerticalScrollbar( |
||||
modifier = Modifier.align(Alignment.CenterEnd) |
||||
.fillMaxHeight(), |
||||
adapter = rememberScrollbarAdapter(stateVertical) |
||||
) |
||||
HorizontalScrollbar( |
||||
modifier = Modifier.align(Alignment.BottomStart) |
||||
.fillMaxWidth() |
||||
.padding(end = 12.dp), |
||||
adapter = rememberScrollbarAdapter(stateHorizontal) |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun TextBox(text: String = "Item") { |
||||
Box( |
||||
modifier = Modifier.height(32.dp) |
||||
.width(400.dp) |
||||
.background(color = Color(200, 0, 0, 20)) |
||||
.padding(start = 10.dp), |
||||
contentAlignment = Alignment.CenterStart |
||||
) { |
||||
Text(text = text) |
||||
} |
||||
} |
||||
``` |
||||
|
||||
Scrollbars can be moved by dragging the bars and using the mouse wheel or the touchpad. Horizontal scrolling with the mouse wheel can be performed by side-clicking the wheel or by holding down `Shift`. |
||||
|
||||
<img alt="Scrollbars" src="scrollbars.gif" height="412" /> |
||||
|
||||
## Lazy scrollable components with Scrollbar |
||||
|
||||
You can use scrollbars with lazy scrollable components, for example, `LazyColumn`. |
||||
|
||||
```kotlin |
||||
import androidx.compose.foundation.VerticalScrollbar |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxHeight |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.lazy.LazyColumn |
||||
import androidx.compose.foundation.lazy.rememberLazyListState |
||||
import androidx.compose.foundation.rememberScrollbarAdapter |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.window.Window |
||||
import androidx.compose.ui.window.application |
||||
import androidx.compose.ui.window.rememberWindowState |
||||
|
||||
fun main() = application { |
||||
Window( |
||||
onCloseRequest = ::exitApplication, |
||||
title = "Scrollbars", |
||||
state = rememberWindowState(width = 250.dp, height = 400.dp) |
||||
) { |
||||
LazyScrollable() |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun LazyScrollable() { |
||||
Box( |
||||
modifier = Modifier.fillMaxSize() |
||||
.background(color = Color(180, 180, 180)) |
||||
.padding(10.dp) |
||||
) { |
||||
|
||||
val state = rememberLazyListState() |
||||
|
||||
LazyColumn(Modifier.fillMaxSize().padding(end = 12.dp), state) { |
||||
items(1000) { x -> |
||||
TextBox("Item #$x") |
||||
Spacer(modifier = Modifier.height(5.dp)) |
||||
} |
||||
} |
||||
VerticalScrollbar( |
||||
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), |
||||
adapter = rememberScrollbarAdapter( |
||||
scrollState = state |
||||
) |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun TextBox(text: String = "Item") { |
||||
Box( |
||||
modifier = Modifier.height(32.dp) |
||||
.fillMaxWidth() |
||||
.background(color = Color(0, 0, 0, 20)) |
||||
.padding(start = 10.dp), |
||||
contentAlignment = Alignment.CenterStart |
||||
) { |
||||
Text(text = text) |
||||
} |
||||
} |
||||
``` |
||||
|
||||
<img alt="Lazy component" src="lazy_scrollbar.gif" height="412" /> |
||||
|
||||
## Tooltips |
||||
|
||||
You can add tooltip to any components using `TooltipArea`. `TooltipArea` is similar to a `Box`, but with the ability to show a tooltip. |
||||
|
||||
The main arguments of the `TooltipArea` function: |
||||
- tooltip - composable content representing tooltip |
||||
- tooltipPlacement - describes how to place tooltip. You can specify an anchor (the mouse cursor or the component), an offset and an alignment |
||||
- delay - time delay in milliseconds after which the tooltip will be shown (default is 500 ms) |
||||
|
||||
```kotlin |
||||
import androidx.compose.foundation.ExperimentalFoundationApi |
||||
import androidx.compose.foundation.TooltipArea |
||||
import androidx.compose.foundation.TooltipPlacement |
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
import androidx.compose.material.Button |
||||
import androidx.compose.material.Surface |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.ExperimentalComposeUiApi |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.draw.shadow |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.unit.DpOffset |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.window.Window |
||||
import androidx.compose.ui.window.application |
||||
import androidx.compose.ui.window.rememberWindowState |
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) |
||||
fun main() = application { |
||||
Window( |
||||
onCloseRequest = ::exitApplication, |
||||
title = "Tooltip Example", |
||||
state = rememberWindowState(width = 300.dp, height = 300.dp) |
||||
) { |
||||
val buttons = listOf("Button A", "Button B", "Button C", "Button D", "Button E", "Button F") |
||||
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { |
||||
buttons.forEachIndexed { index, name -> |
||||
// wrap button in BoxWithTooltip |
||||
TooltipArea( |
||||
tooltip = { |
||||
// composable tooltip content |
||||
Surface( |
||||
modifier = Modifier.shadow(4.dp), |
||||
color = Color(255, 255, 210), |
||||
shape = RoundedCornerShape(4.dp) |
||||
) { |
||||
Text( |
||||
text = "Tooltip for ${name}", |
||||
modifier = Modifier.padding(10.dp) |
||||
) |
||||
} |
||||
}, |
||||
modifier = Modifier.padding(start = 40.dp), |
||||
delayMillis = 600, // in milliseconds |
||||
tooltipPlacement = TooltipPlacement.CursorPoint( |
||||
alignment = Alignment.BottomEnd, |
||||
offset = if (index % 2 == 0) DpOffset(-16.dp, 0.dp) else DpOffset.Zero // tooltip offset |
||||
) |
||||
) { |
||||
Button(onClick = {}) { Text(text = name) } |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
<img alt="Tooltip" src="tooltips.gif" height="314" /> |
Before Width: | Height: | Size: 2.7 MiB |
Before Width: | Height: | Size: 2.7 MiB |
Before Width: | Height: | Size: 2.4 MiB |
@ -1,492 +0,0 @@
|
||||
# Mouse events |
||||
|
||||
## What is covered |
||||
|
||||
In this tutorial we will see how to install mouse event listeners on components |
||||
in Compose for Desktop. |
||||
|
||||
## Mouse event listeners |
||||
|
||||
### Click listeners |
||||
|
||||
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 |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.combinedClickable |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.fillMaxHeight |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
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.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.unit.sp |
||||
import androidx.compose.ui.window.singleWindowApplication |
||||
|
||||
fun main() = singleWindowApplication { |
||||
var count by remember { mutableStateOf(0) } |
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) { |
||||
var text by remember { mutableStateOf("Click magenta box!") } |
||||
Column { |
||||
@OptIn(ExperimentalFoundationApi::class) |
||||
Box( |
||||
modifier = Modifier |
||||
.background(Color.Magenta) |
||||
.fillMaxWidth(0.7f) |
||||
.fillMaxHeight(0.2f) |
||||
.combinedClickable( |
||||
onClick = { |
||||
text = "Click! ${count++}" |
||||
}, |
||||
onDoubleClick = { |
||||
text = "Double click! ${count++}" |
||||
}, |
||||
onLongClick = { |
||||
text = "Long click! ${count++}" |
||||
} |
||||
) |
||||
) |
||||
Text(text = text, fontSize = 40.sp) |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
<img alt="Application running" src="mouse_click.gif" height="500" /> |
||||
|
||||
`combinedClickable` supports only the Primary button (Left Mouse Button) and touch events. If there is a need to handle other buttons differently, please have a look at `Modifier.onClick` below (note: `Modifier.onClick` is currently avilable only for Desktop-JVM platform). |
||||
|
||||
### Mouse move listeners |
||||
|
||||
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 |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.wrapContentSize |
||||
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.graphics.Color |
||||
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 color by remember { mutableStateOf(Color(0, 0, 0)) } |
||||
Box( |
||||
modifier = Modifier |
||||
.wrapContentSize(Alignment.Center) |
||||
.fillMaxSize() |
||||
.background(color = color) |
||||
.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)*. |
||||
|
||||
<img alt="Application running" src="mouse_move.gif" height="519" /> |
||||
|
||||
### Mouse enter listeners |
||||
|
||||
Compose for Desktop also supports pointer enter and exit handlers, like this: |
||||
```kotlin |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
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.ExperimentalComposeUiApi |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.graphics.Color |
||||
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 |
||||
import androidx.compose.ui.window.singleWindowApplication |
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class) |
||||
fun main() = singleWindowApplication { |
||||
Column( |
||||
Modifier.background(Color.White), |
||||
verticalArrangement = Arrangement.spacedBy(10.dp) |
||||
) { |
||||
repeat(10) { index -> |
||||
var active by remember { mutableStateOf(false) } |
||||
Text( |
||||
modifier = Modifier |
||||
.fillMaxWidth() |
||||
.background(color = if (active) Color.Green else Color.White) |
||||
.onPointerEvent(PointerEventType.Enter) { active = true } |
||||
.onPointerEvent(PointerEventType.Exit) { active = false }, |
||||
fontSize = 30.sp, |
||||
fontStyle = if (active) FontStyle.Italic else FontStyle.Normal, |
||||
text = "Item $index" |
||||
) |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
*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)*. |
||||
|
||||
<img alt="Application running" src="mouse_enter.gif" height="500" /> |
||||
|
||||
### 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)*. |
||||
|
||||
### 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 |
||||
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.awt.awtEventOrNull |
||||
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() |
||||
.onPointerEvent(PointerEventType.Press) { |
||||
text = it.awtEventOrNull?.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<String>() } |
||||
|
||||
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) |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### New experimental onClick handlers (only for Desktop-JVM platform) |
||||
`Modifier.onClick` provides independent callbacks for clicks, double clicks, long clicks. It handles clicks originated only from pointer events and accessibility `click` event is not handled out of a box. |
||||
|
||||
Each `onClick` can be configured to target specific pointer events (using `matcher: PointerMatcher` and `keyboardModifiers: PointerKeyboardModifiers.() -> Boolean`). `matcher` can be specified to choose what mouse button should trigger a click. `keyboardModifiers` allows for filtering pointer events which have specified keyboardModifiers pressed. |
||||
|
||||
Multiple `onClick` modifiers can be chained to handle different clicks with different conditions (matcher and keyboard modifiers). |
||||
Unlike `clickable`, `onClick` doesn't have a default `Modifier.indication`, `Modifier.semantics`, and it doesn't trigger a click event when `Enter` pressed. These modifiers need to be added separately if necessary. |
||||
The most generic (with the least number of conditions) `onClick` handlers should be declared before other to ensure correct events propagation. |
||||
|
||||
```kotlin |
||||
import androidx.compose.animation.AnimatedContent |
||||
import androidx.compose.animation.ExperimentalAnimationApi |
||||
import androidx.compose.foundation.ExperimentalFoundationApi |
||||
import androidx.compose.foundation.LocalIndication |
||||
import androidx.compose.foundation.PointerMatcher |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.indication |
||||
import androidx.compose.foundation.interaction.MutableInteractionSource |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.size |
||||
import androidx.compose.foundation.onClick |
||||
import androidx.compose.material.Text |
||||
import androidx.compose.runtime.* |
||||
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.PointerButton |
||||
import androidx.compose.ui.input.pointer.isAltPressed |
||||
import androidx.compose.ui.input.pointer.isShiftPressed |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.window.singleWindowApplication |
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class) |
||||
fun main() = singleWindowApplication { |
||||
Column { |
||||
var topBoxText by remember { mutableStateOf("Click me\nusing LMB or LMB + Shift") } |
||||
var topBoxCount by remember { mutableStateOf(0) } |
||||
// No indication on interaction |
||||
Box(modifier = Modifier.size(200.dp, 100.dp).background(Color.Blue) |
||||
// the most generic click handler (without extra conditions) should be the first one |
||||
.onClick { |
||||
// it will receive all LMB clicks except when Shift is pressed |
||||
println("Click with primary button") |
||||
topBoxText = "LMB ${topBoxCount++}" |
||||
}.onClick( |
||||
keyboardModifiers = { isShiftPressed } // accept clicks only when Shift pressed |
||||
) { |
||||
// it will receive all LMB clicks when Shift is pressed |
||||
println("Click with primary button and shift pressed") |
||||
topBoxCount++ |
||||
topBoxText = "LMB + Shift ${topBoxCount++}" |
||||
} |
||||
) { |
||||
AnimatedContent( |
||||
targetState = topBoxText, |
||||
modifier = Modifier.align(Alignment.Center) |
||||
) { |
||||
Text(text = it, textAlign = TextAlign.Center) |
||||
} |
||||
} |
||||
|
||||
var bottomBoxText by remember { mutableStateOf("Click me\nusing LMB or\nRMB + Alt") } |
||||
var bottomBoxCount by remember { mutableStateOf(0) } |
||||
val interactionSource = remember { MutableInteractionSource() } |
||||
// With indication on interaction |
||||
Box(modifier = Modifier.size(200.dp, 100.dp).background(Color.Yellow) |
||||
.onClick( |
||||
enabled = true, |
||||
interactionSource = interactionSource, |
||||
matcher = PointerMatcher.mouse(PointerButton.Secondary), // Right Mouse Button |
||||
keyboardModifiers = { isAltPressed }, // accept clicks only when Alt pressed |
||||
onLongClick = { // optional |
||||
bottomBoxText = "RMB Long Click + Alt ${bottomBoxCount++}" |
||||
println("Long Click with secondary button and Alt pressed") |
||||
}, |
||||
onDoubleClick = { // optional |
||||
bottomBoxText = "RMB Double Click + Alt ${bottomBoxCount++}" |
||||
println("Double Click with secondary button and Alt pressed") |
||||
}, |
||||
onClick = { |
||||
bottomBoxText = "RMB Click + Alt ${bottomBoxCount++}" |
||||
println("Click with secondary button and Alt pressed") |
||||
} |
||||
) |
||||
.onClick(interactionSource = interactionSource) { // use default parameters |
||||
bottomBoxText = "LMB Click ${bottomBoxCount++}" |
||||
println("Click with primary button (mouse left button)") |
||||
} |
||||
.indication(interactionSource, LocalIndication.current) |
||||
) { |
||||
AnimatedContent( |
||||
targetState = bottomBoxText, |
||||
modifier = Modifier.align(Alignment.Center) |
||||
) { |
||||
Text(text = it, textAlign = TextAlign.Center) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### New experimental onDrag modifier (only for Desktop-JVM platform) |
||||
|
||||
`Modifier.onDrag` allows for configuration of the pointer that should trigger the drag (see `matcher: PointerMatcher`). |
||||
Many `onDrag` modifiers can be chained together. |
||||
|
||||
The example below also shows how to access the state of keyboard modifiers (via `LocalWindowInfo.current.keyboardModifier`) for cases when keyboard modifiers can alter the behaviour of the drag (for example: move an item if we perform a simple drag; or copy/paste an item if dragged with Ctrl pressed) |
||||
|
||||
```kotlin |
||||
import androidx.compose.foundation.ExperimentalFoundationApi |
||||
import androidx.compose.foundation.PointerMatcher |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.gestures.onDrag |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.offset |
||||
import androidx.compose.foundation.layout.size |
||||
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.geometry.Offset |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.input.pointer.PointerButton |
||||
import androidx.compose.ui.input.pointer.isCtrlPressed |
||||
import androidx.compose.ui.platform.LocalWindowInfo |
||||
import androidx.compose.ui.unit.IntOffset |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.window.singleWindowApplication |
||||
|
||||
@OptIn(ExperimentalFoundationApi::class) |
||||
fun main() = singleWindowApplication { |
||||
val windowInfo = LocalWindowInfo.current |
||||
|
||||
Column { |
||||
var topBoxOffset by remember { mutableStateOf(Offset(0f, 0f)) } |
||||
|
||||
Box(modifier = Modifier.offset { |
||||
IntOffset(topBoxOffset.x.toInt(), topBoxOffset.y.toInt()) |
||||
}.size(100.dp) |
||||
.background(Color.Green) |
||||
.onDrag { // all default: enabled = true, matcher = PointerMatcher.Primary (left mouse button) |
||||
topBoxOffset += it |
||||
} |
||||
) { |
||||
Text(text = "Drag with LMB", modifier = Modifier.align(Alignment.Center)) |
||||
} |
||||
|
||||
var bottomBoxOffset by remember { mutableStateOf(Offset(0f, 0f)) } |
||||
|
||||
Box(modifier = Modifier.offset { |
||||
IntOffset(bottomBoxOffset.x.toInt(), bottomBoxOffset.y.toInt()) |
||||
}.size(100.dp) |
||||
.background(Color.LightGray) |
||||
.onDrag( |
||||
enabled = true, |
||||
matcher = PointerMatcher.mouse(PointerButton.Secondary), // right mouse button |
||||
onDragStart = { |
||||
println("Gray Box: drag start") |
||||
}, |
||||
onDragEnd = { |
||||
println("Gray Box: drag end") |
||||
} |
||||
) { |
||||
val keyboardModifiers = windowInfo.keyboardModifiers |
||||
bottomBoxOffset += if (keyboardModifiers.isCtrlPressed) it * 2f else it |
||||
} |
||||
) { |
||||
Text(text = "Drag with RMB,\ntry with CTRL", modifier = Modifier.align(Alignment.Center)) |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
There is also a non-modifier way to handle drags using `suspend fun PointerInputScope.detectDragGestures`: |
||||
|
||||
```kotlin |
||||
import androidx.compose.foundation.ExperimentalFoundationApi |
||||
import androidx.compose.foundation.PointerMatcher |
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.gestures.detectDragGestures |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.offset |
||||
import androidx.compose.foundation.layout.size |
||||
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.geometry.Offset |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.input.pointer.pointerInput |
||||
import androidx.compose.ui.unit.IntOffset |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.compose.ui.window.singleWindowApplication |
||||
|
||||
@OptIn(ExperimentalFoundationApi::class) |
||||
fun main() = singleWindowApplication { |
||||
var topBoxOffset by remember { mutableStateOf(Offset(0f, 0f)) } |
||||
|
||||
Box(modifier = Modifier.offset { |
||||
IntOffset(topBoxOffset.x.toInt(), topBoxOffset.y.toInt()) |
||||
}.size(100.dp) |
||||
.background(Color.Green) |
||||
.pointerInput(Unit) { |
||||
detectDragGestures( |
||||
matcher = PointerMatcher.Primary |
||||
) { |
||||
topBoxOffset += it |
||||
} |
||||
} |
||||
) { |
||||
Text(text = "Drag with LMB", modifier = Modifier.align(Alignment.Center)) |
||||
} |
||||
} |
||||
``` |
||||
|
Before Width: | Height: | Size: 110 KiB |
Before Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 257 KiB |