Composeでマウスイベントと修飾キーを取得する

この記事をシェア

今回の記事では、すこしニッチな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.pointerInputPointerEventについては、2024年のDroidKaigiで詳しく説明したので、そちらも参考にしていただければと思います。(動画スライド

さて、タッチイベントでは扱うPointerEventTypeは基本的にPressMoveReleaseの3種類でした。しかしマウスイベントの場合は種類が増えます。

  • Enter:マウスカーソルがコンポーザブルの領域に入った
  • Exit:マウスカーソルがコンポーザブルの領域から出た
  • Press:マウスのボタンが押された
  • Release:マウスのボタンが離された
  • Move:マウスカーソルが動いた
  • Scroll:マウスホイールが操作された

Moveイベントは、マウスのボタンが押されているかどうかに関係なく発生します。また、コンポーザブル領域外でボタンを押してもPressイベントは発生しませんが、コンポーザブル領域内でボタンを押したまま領域外までドラッグした場合は、ボタンを押し続けている限りMoveイベントが継続し、Releaseイベントが最後に発生して終了します。

押されたボタンの判別

右クリックと左クリックの判別には、PointerButtonsisPrimaryPressedisSecondaryPressedを利用できます。(左右を入れ替える設定をしていなければ、)左ボタンを押した時にisPrimaryPressedtrueになり、右ボタンを押した時にisSecondaryPressedtrueになります。

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クラスにはisCtrlPressedisShiftPressedなどのプロパティが定義されており、これらの値を確認することによって、修飾キーが押されているかどうかを確認できます。

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 ***
...

MoveScrollEnterExitイベントは、ボタンが押されていない状態でも発生することに注意が必要です。

LocalWindowInfoは使えない(っぽい)

私が修飾キーの状態を取得する方法を調べたときに、最初に見つけたのはLocalWindowInfoを利用する方法でした。しかしこの方法は、私が試した限りでは、期待通りに動作しませんでした。

Androidでは、画面にTextFieldを配置してフォーカスを当てないと修飾キーの状態を取得できませんでした。

Desktopでは、Ctrlキーを押している間、pointerInputが動作しなくなりました。

これらの現象が仕様なのかバグなのかは確認できていませんが、マウスイベント発生時の修飾キーの状態を取得するには、PointerEvent.keyboardModifiersを使う方がよさそうです。

この記事をシェア