torutkのブログ

ソフトウェア・エンジニアのブログ

JavaFXで作ったアナログ時計にタッチパネル操作を追加してみた際のメモ

 この記事は、JavaFX Advent Calendar 2015 の二日目の記事です。一日目のJavaFXで流れるHello world - Qiitaも実は私が書いています。三日目は@btnrougeさんのMSI/EXE インストーラ版 Scene Builder を HiDPI 対応させるには - notepadです。

 今年の春、JJUG CCC 2015 Springにおいて「JavaFXグラフィックスとアニメーション入門 デスクトップにアナログ時計を出してみよう」の発表を行いました。
http://d.hatena.ne.jp/torutk/20150412/p1

 アナログ時計の構成は、左の図のようになります。これを作成し、ウィンドウ枠や背景を透明化して、デスクトップに表示する時計を作ろうというものでした。このときは、時計の表示(描画)を実装するのに一杯で、タッチパネル対応までは手が回っていませんでした。

 その後、ちょっとずつタッチパネル操作を追加し、おかしな挙動をしらべてみたりとしてようやく一通りの対応ができるようになりました。本日はタッチパネル対応の内容を簡単に紹介します。

タッチパネルの操作用語

 最初に、タッチパネルの操作用語を整理します。タッチパネルの操作には、似たような言葉と操作を多数見かけます。例えばタッチパネル上を一箇所触る操作に対しては、タッチ、タップであるとか、一箇所を長押しする操作に対しては、ロングタップ、タッチ・アンド・ホールドであるとか、タッチパネル上で指を触ったままずらす操作に対しては、ドラッグ、フリック、スワイプであるとかです。

操作内容 Apple用語 Android用語 Microsoft用語 JavaFX用語
1本指で触りすぐに離す タップ タッチ タップ タッチ
1本指で触りすぐに離すを2回連続 ダブルタップ ダブルタッチ
1本指で触ったままずらす ドラッグ ドラッグ、スワイプ スライド スクロール
1本指で触ってはらう フリック フリング スワイプ スワイプ
2本指で触ったまま間を広げる ピンチオープン(ピンチアウト) ピンチオープン ストレッチ ズーム
2本指で触ったまま間を狭める ピンチクローズ(ピンチイン) ピンチクローズ ピンチ ズーム
1本指で触りしばらくして離す タッチ・アンド・ホールド ロングプレス 長押し
2本指で触ったまま回転させる 回転 ローテート 回転 回転

上述の表の作成には、次の文献を参照しました。

Android用語では、他にもツーフィンガーロングプレスドラッグなど2本指の操作も多数定義されていますがここでは割愛しました。
Microsoft用語でも文献によっては別な用語を使っていたりと定まっていない部分もあるようです。

JavaFXプログラムでの入力イベントの確認

JavaFXのプログラムで、発生する入力イベントを全て取得しログ表示したい場合は、Stage#addEventFilterメソッドにイベント種類とハンドラーを指定します。

Applicationクラスを継承したクラスのstartメソッド内で、引数に渡されるStageインスタンスに対して、とりあえずイベントをすべてprintln出力するようにしたコードを次に示します。

public class AnalogClock extends Application {
    @Override
    public void start(Stage primaryStage) {
        :
        primaryStage.addEventFilter(EventType.ROOT, System.out::println);
    }
    :
}

アナログ時計に実装したいタッチパネル操作

今回、アナログ時計に実装したいタッチパネル操作は次の3つです。

  1. ドラッグでデスクトップ上の表示位置を移動させる
  2. ピンチ(ズーム)で時計の大きさを増減させる
  3. 長押しでポップアップメニューを表示させ、メニュー上の項目を選択させる

ドラッグでデスクトップ上の表示位置を移動

タッチパネル上でドラッグ操作を実施すると、JavaFXではTouchEvent、ScrollEvent、MouseEventが発生します。MouseEventでは、MOUSE_DRAGGEDが発生しました。

JavaFXのガイド文書「JavaFX: イベントの処理」5章 タッチ対応デバイスからのイベントの使用 に説明がありました。引用します。

ジェスチャおよびタッチが実行されたときには、対応するジェスチャ・イベントまたはタッチ・イベントだけでなく、その他のタイプのイベントも生成できます。スワイプ・ジェスチャでは、スワイプ・イベントだけでなく、スクロール・イベントも生成されます。

タッチ画面へのタッチでは、対応するマウス・イベントも生成されます。たとえば、画面上のポイントにタッチすると、TOUCH_PRESSEDイベントとMOUSE_PRESSEDイベントが生成されます。画面上の単一ポイントを移動すると、スクロール・イベントとドラッグ・イベントが生成されます。タッチ・イベントまたはジェスチャ・イベントをアプリケーションで直接処理できない場合でも、タッチの実行時に生成されるマウス・イベントに対するレスポンスを生成するように変更するだけで、タッチ対応デバイスでアプリケーションを実行できるようになります。

ということで、従来のマウスでのドラッグイベントに対応していれば、そのままのコードでタッチパネル上でのドラッグ操作にも対応することができます。
つまり、タッチパネル操作に対応するからといってタッチパネル専用のコードを必ずしも書く必要はないということでした。

一応、マウスのドラッグ操作でウィンドウを移動させるコードを次に示します。

public class AnalogClock extends Application {
    private double dragStartX;
    private double dragStartY;
        :
    @Override
    public void start(Stage primaryStage) {
        Scene scene = ...
          :
        // マウスのドラッグ操作でウィンドウを移動
        scene.setOnMousePressed(e -> {
            dragStartX = e.getSceneX();
            dragStartY = e.getSceneY();
        });
        scene.setOnMouseDragged(e -> {
            primaryStage.setX(e.getScreenX() - dragStartX);
            primaryStage.setY(e.getScreenY() - dragStartY);
        });
          :
    }

ピンチ(ズーム)で時計の大きさを増減

タッチパネル上でピンチ操作を実施すると、JavaFXではTouchEvent、ScrollEvent、RotateEvent、ZoomEvent、MouseEventが発生しました。

今回は直球でZoomEventを取得し、ウィンドウサイズの変更を行います。

    public void start(Stage primaryStage) throws Exception {
        :
        // タッチパネルのピンチ操作でウィンドウサイズを変更
        scene.setOnZoom(e -> {
            zoom(e.getZoomFactor());
        });
        :
    }

    private void zoom(double factor) {
        double scale = root.getScaleX() * factor;
        scale = Math.max(Math.min(scale, MAX_SCALE), MIN_SCALE);
        root.setScaleX(scale);
        root.setScaleY(scale);
        stage.setWidth(INITIAL_WINDOW_SIZE * scale);
        stage.setHeight(INITIAL_WINDOW_SIZE * scale);
    }

マウスホイールで表示サイズ変更を実装していた場合の注意点

よく、マウスホイールで拡大・縮小表示するUIを見かけます。割と使い勝手がいいので、このUIは重宝します。しかし、JavaFXではマウスホイールを直接イベントとして取ることができないので、マウスホイールを動かしたときに生成されるScrollEventを捉えて表示サイズ変更を行います。

        // 時計のサイズを変更する
        // マウスのホイール操作によるScrollEventを選別してウィンドウサイズを変更
        scene.setOnScroll(e -> {
            double zoomFactor = e.getDeltaY() > 0 ? 1.1 : 0.9;
            zoom(zoomFactor);
        });

しかし、このコードをタッチパネル上で実行すると、おかしな振る舞いをしてしまいます。タッチパネル上のドラッグやスライド(スワイプ)操作をしたときにもScrollEventが発生するので、それをホイールと誤認してウィンドウサイズを変更してしまうからです。

マウスのホイール操作で発生するScrollEventと、それ以外(タッチパネル上のドラッグ操作など)の操作で発生するScrollEventを区別します。区別する方法を探してみたところ、JavaFXのガイド文書「JavaFX: イベントの処理」5章 タッチ対応デバイスからのイベントの使用 に記述があります。引用します。

スクロール・ジェスチャが実行されると、SCROLL_STARTED、SCROLLおよびSCROLL_FINISHEDイベントが生成されます。マウス・ホイールの移動では、SCROLLイベントのみが生成されます。

まあ、何となく実装もできますが、イベントの状態を管理しなくてはならず面倒そうです。
と思っていたら、他にもうちょっと楽な方法がありました。その方法はゆっち氏のブログ(次のURL)に書かれていました。
http://yucchi.jp/blog/?p=1750

タッチカウントと慣性スクロールイベントで拡大縮小処理を行うかどうか判断させています。

つまり、ScrollEventの属性でタッチ数を見て、それが0個であればタッチではなくマウスホイール操作によるイベントと判定、ただし、スワイプ操作で発生する慣性に基づくScrollEventも含まれてしまうので、それを除外する、という処理になります。言葉では複雑ですがコードはif文1つを追加で済みます。

        // 時計のサイズを変更する
        // マウスのホイール操作によるScrollEventを選別してウィンドウサイズを変更
        scene.setOnScroll(e -> {
            if (e.getTouchCount() != 0 || e.isInertia()) return;  // 追加行
            double zoomFactor = e.getDeltaY() > 0 ? 1.1 : 0.9;
            zoom(zoomFactor);
        });
(追記)TOUCHイベントを使った判別方法

id:skrbさんがWheel or Touch - JavaFX in the Boxで、タッチ操作によるScrollイベントか否かを判別する汎用的な方法を紹介しています。この方法は、Scrollイベント以外でもタッチ操作か否かを判断することができるので、タッチパネルUIを扱うならば知っておくべき方法です。

長押しでポップアップメニューを表示させ、メニュー上の項目を選択

これは実現方法が分からずしばらく悩みました。ジェスチャには長押しは見当たらず、低レベルなTouchEventを見ると、TOUCH_STATIONARYというタッチ・ポイントが押されたまま停止しているときに発生するイベントがありました。

最初これを実装してみましたが、タッチパネル上で長押し操作だけでなく、ドラッグやピンチといった操作をしているときも指の移動が遅くなると発生してしまいます。

いろいろ探してみたところ、Windows OSのタッチ機能に、長押しを検出すると右クリックのイベントを発行するそうです。

そこで、コンテキストメニューとしてポップアップメニューを登録することで実現ができました。

        ContextMenu popup = new ContextMenu();
            :
        // コンテキストメニュー操作(OS依存)をしたときに、ポップアップメニュー表示
        // Windows OSでは、マウスの右クリック、touchパネルの長押しで発生
        root.setOnContextMenuRequested(e -> {
            popup.show(primaryStage, e.getScreenX(), e.getScreenY());
        });

これも結局タッチパネル操作対応は直接していません。

まとめ的なもの

JavaFXのタッチパネル操作対応は、まず用語/機能が世の中一般的に広まっているタッチパネルの操作と直接対応していないものがあります。

次にタッチパネル操作によって、タッチパネル固有のイベントだけではなく、マウスイベントも発生します。このあたりを把握しておかないと、タッチパネル操作に対応できなかったり、おかしな挙動をする破目に会うことがあります。

アナログ時計のURL

JJUG CCC以降のアナログ時計はGithubリポジトリに上げてタッチパネル操作ほかの対応を入れています。
https://github.com/torutk/analogclock

コードの説明はWikiに記載しています。
JavaFXとアナログ時計 - ソフトウェアエンジニアリング - Torutk