🐭

Flutter で自作マウスカーソルを表示する

2020/10/23に公開

目的

Flutter で自作のマウスカーソルを表示したい時があります。

こんなのを作ります。

サンプルコード

サンプルコードはこちらに置いてます。
https://github.com/HeavenOSK/flutter_ipad_mouse_cursor/tree/master/custom_mouse_cursor

用語

この記事では、以下の用語を次の意味で使います。

  • ポインタ
    • マウスの操作実際の位置
  • マウスカーソル
    • マウスの位置を示すアイコン(デフォルトは大体矢印のやつ)

やり方(概要)

  1. アプリ上でマウスカーソルを消す
  2. 自作のマウスカーソルを表示するレイヤを追加する
  3. 自作のマウスカーソルの位置を更新する

1.アプリ上でマウスカーソルを消す

ポインタ/マウスカーソルを扱うのに便利なMouseRegionという Widget が用意されています。
MouseRegionで Widget を囲めば、対象の Widget 上にポインタが重なった時に検知することができます。

MouseRegion の機能

  1. onEnter, onExit, onHover コールバックで、ポインタが MouseRegion 上にある時のイベントを受け取ることができます。
  2. cursorを指定することで、ポインタが MouseRegion 上にある時の見た目を変更することができます。(矢印アイコンから 👈 アイコンへの変更など)

マウスカーソルを消すためには、MouseRegion#cursorSystemMouseCursors.none を指定します。

return MouseRegion(
    cursor: SystemMouseCursors.none,
    child: SomeChildWidget(),
);

今回は、アプリ全体でマウスカーソルを消したいので、次のように MaterialApp#builder内で MouseRegion で囲んであげます。

return MaterialApp(
    title: 'Flutter Demo',
    builder: (_, child) => MouseRegion(
        cursor: SystemMouseCursors.none,
        child: child,
    ),
    home: const Home(),
);

これでマウスカーソルが表示されなくなります。

2.自作のマウスカーソルを表示するレイヤを追加する

自作のマウスカーソルの表示は、Stackで一番上の層に自作マウスカーソルを置くことで実現します。
今回はアプリ全体で表示したいので、MouseRegion と同じく、MaterialApp#builder内で囲んであげます。
 
こんな Widget を用意して...

class CustomMouseCursorOverlayer extends StatelessWidget {
  const CustomMouseCursorOverlayer({
    Key key,
     this.child,
  }) : super(key: key);

  final Widget child;

  
  Widget build(BuildContext context) {
    return MouseRegion(
      cursor: SystemMouseCursors.none,
      child: Stack(
        children: [
          child,
	  // ↓マウスカーソル↓
          const CustomMouseCursor(),
        ],
      ),
    );
  }
}

MaterialApp#builder 内で囲みます。

return MaterialApp(
    title: 'Flutter Demo',
    builder: (_, child) => CustomMouseCursorOverlayer(child: child),
    home: const Home(),
);

これで自作マウスカーソルを表示する土台ができました。

(自作マウスカーソル自体は普通の Widget ですので載せていませんが、気になる方はこちら をご覧ください)

3.自作のマウスカーソルの位置を更新する

MouseRegion#onHover でポインタの位置を状態管理クラスに伝えます。
マウス操作のイベントで更新頻度が高いため、Stack を使用している箇所では更新をかけずに、マウスカーソルだけを更新するようにした方がいいです。

更新を伝える箇所はこのようになります。
(今回は Riverpod を使っていますが、Provider パッケージでも素の InheritedWidget を使ったパターンでも何を使っても問題ないと思います)

  
  Widget build(BuildContext context) {
    final controller = useProvider(customMouseCursorController);
    return MouseRegion(
      cursor: SystemMouseCursors.none,
      onHover: (event) {
        controller.updatePosition(event.position);
      },
      child: Stack(
        children: [
          child,
          const PositionedCursor(),
        ],
      ),
    );
  }

PositionedCursor の中身は以下のようになっています。ポインタの位置変更を状態クラスを介して受けとって更新をかけています。
状態管理クラスから受け取った位置から自作マウスカーソルの半径分ズラしているのは、自作マウスカーソルの中心位置を実際のマウスポインタの位置に合わせるためです。


Widget build(BuildContext context) {
  final cursorState = useProvider(customMouseCursorController.state);
  return Positioned(
    top: cursorState.position.dy - CustomMouseCursor.radius / 2,
    left: cursorState.position.dx - CustomMouseCursor.radius / 2,
    child: const CustomMouseCursor(),
  );
}

Tips

アプリからポインタが外れた時に、自作のマウスカーソルが残ってしまうので、MouseRegion#onExit のイベント発火時に、マウスカーソルを消してあげる処理を入れるといいと思います。


Widget build(BuildContext context) {
  final controller = useProvider(customMouseCursorController);
  return MouseRegion(
    onExit: (_) => controller.exit(),
    // ...省略
  );
}

完成!

以上で完成です。

サンプルはこちらに上げてます。
https://github.com/HeavenOSK/flutter_ipad_mouse_cursor/tree/master/custom_mouse_cursor

iPad のマウスカーソルを Flutter で再現するチャレンジをやっているので、良かったら Twitter をフォローしてください。

Discussion