今回の記事では、すこしニッチなComposeの話題を扱います。ComposeのUIでクリックやスクロールホイールなどのマウスイベントを取得する方法と、そのイベント発生時にキーボードでCtrlやShiftなどの修飾キーが押されているかどうかを取得する方法を紹介します。
目次
マウスとキーボードへのComposeの対応状況
Composeは、マウスとキーボードのイベントに対応しています。Androidでは、Bluetoothなどで接続したマウスやキーボードを利用できます。Compose Multiplatformは、以前からDesktopやWebでのマウスやキーボード操作に対応していました。さらに、Compose Multiplatform 1.8.0でiOSにも対応しました。これで一通りのプラットフォームでマウスとキーボードに対応できるようになりました。
マウスイベントの取得
マウスイベントは、タッチイベントと同じようにModifier.pointerInput
のスコープでawaitPointerEvent
を呼び出して取得します。Modifier.pointerInput
を適用したコンポーザブル関数の範囲内でマウスを操作すると、PointerEvent
が発生します。
Box(
modifier = Modifier
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
println("*** ${event.type} ***")
}
}
}
)
Modifier.pointerInput
やPointerEvent
については、2024年のDroidKaigiで詳しく説明したので、そちらも参考にしていただければと思います。(動画、スライド)
さて、タッチイベントでは扱うPointerEventType
は基本的にPress
、Move
、Release
の3種類でした。しかしマウスイベントの場合は種類が増えます。
Enter
:マウスカーソルがコンポーザブルの領域に入ったExit
:マウスカーソルがコンポーザブルの領域から出たPress
:マウスのボタンが押されたRelease
:マウスのボタンが離されたMove
:マウスカーソルが動いたScroll
:マウスホイールが操作された
Move
イベントは、マウスのボタンが押されているかどうかに関係なく発生します。また、コンポーザブル領域外でボタンを押してもPress
イベントは発生しませんが、コンポーザブル領域内でボタンを押したまま領域外までドラッグした場合は、ボタンを押し続けている限りMove
イベントが継続し、Release
イベントが最後に発生して終了します。
押されたボタンの判別
右クリックと左クリックの判別には、PointerButtons
のisPrimaryPressed
とisSecondaryPressed
を利用できます。(左右を入れ替える設定をしていなければ、)左ボタンを押した時にisPrimaryPressed
がtrue
になり、右ボタンを押した時にisSecondaryPressed
がtrue
になります。
val event = awaitPointerEvent()
if (event.type == PointerEventType.Press) {
if (event.buttons.isPrimaryPressed) {
println("*** 左ボタン ***")
}
if (event.buttons.isSecondaryPressed) {
println("*** 右ボタン ***")
}
}
ホイールの回転の判別
スクロールホイールの回転の向きと量は、PointerInputChange.scrollDelta
.yで取得できます。回転の向きによってscrollDelta
の正負が入れ替わり、回転量によって絶対値が変化します。
val event = awaitPointerEvent()
if (event.type == PointerEventType.Scroll) {
println("*** Scroll ${event.changes[0].scrollDelta.y}")
}
カーソル位置の取得
カーソル位置の取得は、タッチイベントと同様に、PointerInputChange.position
を利用します。
val event = awaitPointerEvent()
if (event.type == PointerEventType.Press) {
println("*** Press at ${event.changes[0].position}")
}
修飾キーの状態の取得
マウスイベント発生時のCtrlやShiftなどの修飾キーの状態は、PointerEvent.keyboardModifiers
で取得できます。PointerKeyboardModifiers
クラスにはisCtrlPressed
やisShiftPressed
などのプロパティが定義されており、これらの値を確認することによって、修飾キーが押されているかどうかを確認できます。
val event = awaitPointerEvent()
if (event.type == PointerEventType.Press) {
when {
event.keyboardModifiers.isCtrlPressed -> println("Ctrl+クリック")
event.keyboardModifiers.isShiftPressed -> println("Shift+クリック")
else -> println("クリック")
}
}
注意点
ここからは、Composeでマウスイベントや修飾キーの状態を取得するときの注意点を2つ紹介します。
pressedが前提のコードに注意
タッチイベントの実装では、以下のようにすべての指が離れた後に何らかの処理を行うパターンがよくあります。
pointerInput(Unit) {
awaitEachGesture {
do {
val event = awaitPointerEvent()
println("*** ${event.type} ***")
} while (event.changes.any { it.pressed })
println("*** Gesture End ***")
}
}
このコードはタッチイベントでは期待通りに動作します。しかし、マウスでボタンを押さずにカーソルを動かすと、Move
イベントが1回発生するたびに「Gesture End」が出力されてしまいます。
*** Enter
*** Gesture End ***
*** Move
*** Gesture End ***
*** Move
*** Gesture End ***
*** Move
*** Gesture End ***
*** Move
*** Gesture End ***
*** Move
*** Gesture End ***
...
Move
、Scroll
、Enter
、Exit
イベントは、ボタンが押されていない状態でも発生することに注意が必要です。
LocalWindowInfoは使えない(っぽい)
私が修飾キーの状態を取得する方法を調べたときに、最初に見つけたのはLocalWindowInfo
を利用する方法でした。しかしこの方法は、私が試した限りでは、期待通りに動作しませんでした。
Androidでは、画面にTextField
を配置してフォーカスを当てないと修飾キーの状態を取得できませんでした。
Desktopでは、Ctrlキーを押している間、pointerInput
が動作しなくなりました。
これらの現象が仕様なのかバグなのかは確認できていませんが、マウスイベント発生時の修飾キーの状態を取得するには、PointerEvent.keyboardModifiers
を使う方がよさそうです。