プロジェクト

全般

プロフィール

JavaFXとアナログ時計

はじめに

JavaFXのグラフィックスとアニメーションの機能を使ってアナログ時計(文字盤に長針、短針、秒針を表示し、時刻とともに各針を動かす)を作成してみます。

アナログ時計は次の3種類作成します。

  • AnalogClockImaging
    • 時計盤、長針、短針、秒針、中心のそれぞれを画像で用意し、それを使って実現する
  • AnalogClockDrawing
    • 時計盤、長針、短針、秒針、中心のそれぞれをグラフィックスで描画して実現する
  • AnalogClockSvg
    • 時計盤、長針、短針、秒針、中心のそれぞれをSVGデータで用意し、それを使って実現する

開発作業の履歴

アナログ時計プログラムの作成作業の履歴は、チケット #19 に記載しています。

AnalogClockImaging プログラムの構成

プログラムを格納したリポジトリは次です。

時計の構成

時計盤、短針、長針、秒針、中心の止め釘をそれぞれ独立した画像として用意し、これらをを重ねることでアナログ時計を作成します。

パーツ 画像例
時計盤
短針
長針
秒針
中心

各画像は背景を透過色にしておいて、順番に重ねて表示すると時計になります。

analogclock.png

画像を読み込み重畳

次のプログラムコードは、画像ファイルを読み込み、

        StackPane root = new StackPane();

        ImageView clockDial = new ImageView(new Image("file:clockDial.png"));
        ImageView hourHand = new ImageView(new Image("file:clockHourHand.png"));
        ImageView minuteHand = new ImageView(new Image("file:clockMinuteHand.png"));
        ImageView secondsHand = new ImageView(new Image("file:clockSecondsHand.png"));
        ImageView centerPoint = new ImageView(new Image("file:clockCenterPoint.png"));

        root.getChildren().addAll(
                clockDial, hourHand, minuteHand, secondsHand, centerPoint
        );
        Scene scene = new Scene(root);

javafx.scene.image.Imageクラスは、URL文字列またはInputStreamで指定した画像を読み込みます。
上述はプログラムを実行した環境でのカレントディレクトリにファイルがある場合のURL文字列指定です。
実行可能JARファイルに画像を含める際は、InputStreamで指定します。

針の回転

短針、長針、秒針をそれぞれ12時間、60分、60秒で360度回転させることで時計の動作が実現できます。
JavaFXではアニメーション機能を使ってNodeを回転させることができます。

秒針の回転

アニメーションの実現には、

  • Transition系クラスを使う
  • Timelineクラスを使う
  • AnimationTimerクラスを使う

があります。上から下へいくにつれ低レイヤー(複雑)になっていきます。

画像で針を回転させる場合、今回の画像では回転中心と画像中心が一致しているので、RotateTransitionクラスが使えます。

RotationTransitionクラスを使って、秒針を回転させるのは次のコードになります。

        RotateTransition rt = new RotateTransition(Duration.seconds(60), secondsHand);
        rt.setByAngle(360);
        rt.setCycleCount(Animation.INDEFINITE);
        rt.setInterpolator(Interpolator.LINEAR);
        rt.play();

秒針は60秒間で一回転(360度)するので、RotateTransitionに対してアニメーション時間を60秒、その時間でRotate(回転)する量をsetByAngleメソッドで360度指定しています。
秒針は1回転しても止まらず次の回転をするので、setCycleCountメソッドでアニメーション実施回数を無期限(INDEFINITE)にしています。
1回のサイクル中は位置によらず同じペースで回転してほしいので、setInterpolatorメソッドでLINEARを指定しています。デフォルトはInterpolator.EASE_BOTHとなり、これはアニメーション開始時はゆっくりと動き、真ん中では最高速となり、アニメーション終了時にむけてだんだんゆっくり動きます。

分針の回転

分針は1時間(60分間)で1回転(360度)するので、RotateTransitionのコンストラクタでアニメーション時間を60分で指定しています。分針minuteHandにこれを設定します。それ以外は秒針と同じです。

時針の回転

時針は12時間で1回転するので、RotateTransitionのコンストラクタでアニメーション時間を12時間で指定しています。時針hourHandにこれを設定します。それ以外は秒針・分針と同じです。

現在時刻の表示

時計のアニメーション開始時は、上述では0時0分0秒となっているので、コンピュータ時刻に合わせて動かすようにします。時針、分針、秒針についてそれぞれ現在時刻から針の初期角度を算出します。次にそれをRotateTransition#setFromAngleメソッドで指定します(単位は度)。

秒針の初期角度

現在時刻から秒を取り出し、60秒で360度進むので360/60を乗じます。次にコードを示します。

private static double getAngleOfSeconds(LocalTime time) {
    return time.getSecond() * 360 / 60;
}
  • 360 / 60は割り切れるので整数のまま演算しています。

分針の初期角度

現在時刻から分と秒を取り出し、分と秒をまず分単位に合わせます。分針は60分で360度進むので、360/60を乗じます。次にコードを示します。

private static double getAngleOfMinute(LocalTime time) {
    return (time.getMinute() + time.getSecond() / 60d) * 360 / 60;
}
  • 秒を60で割ると小数点になるので60dと浮動小数点数で演算しています。

時針の初期角度

現在時刻から時と分と秒を取り出し、時については12時間で1周するので12で剰余を取り、それと分と秒を時単位に合わせます。それから360/12を乗じます。コードを次に示します。

private static double getAngleOfHour(LocalTime time) {
    return (time.getHour() % 12 + time.getMinute() / 60d + time.getSecond() / (60d * 60d)) * 360 / 12;
}

アナログ時計動作イメージ

次は、アナログ時計のプログラムを実行し、動画キャプチャツールでキャプチャしてMacromedia Flash形式で保存した動画です。毎秒16フレームで作成しました。

Your browser cannot display Flash content, please check that Adobe Flash Player is installed
Get Flash

秒針の動きを見ると分かる通り、非常にスムーズに動いています。もし、JavaFXのアニメーション機能ではなく、アプリケーションでスレッドを使って短い間隔で描画しても、カクカクになると思われます。

時刻の正確性

このアナログ時計プログラムは、プログラム開始時にシステム時刻を取得した後は、JavaFXのアニメーション機能で指定したDurationで時を刻んでいきます。そのため、長期間動かしているとシステム時刻からずれていくと思われます。

どのくらいずれるのかは実行環境依存ですが、開発マシンでは24時間動かしっぱなしにして数十秒のずれでした。

AnimationTimerを付加すると、アニメーション・フレームの描画タイミングでコールバック・メソッドが呼ばれるので、そこでタイミングの補正が可能となります。コールバック・メソッドの引数にはSystem.nanoTime()相当の値が渡されます。

無理やりなサンプルですが、次のコードを追加すると、アニメーション・フレーム毎にhandleメソッドが呼ばれます。このサンプルでは、10億ナノ秒(以上)に1回、print文を実行します。16msごとに呼ばれるメソッドなので、極力最小限の処理にとどめます。

    new AnimationTimer() {
        long prev;
        @Override
        public void handle(long now) {
            if (now - prev > 1_000_000_000) {
                prev = now;
                System.out.println("AnimationTimer#handle: " + now);
            }
        }

    }.start();

AnalogClockDrawing プログラムの構成

プログラムを格納したリポジトリは次です。

時計の構成

時計盤、短針、長針、秒針、中心の止め釘をそれぞれ独立したNodeとして作成し、これらをを重ねることでアナログ時計を作成します。

        Group root = new Group();
        root.getChildren().addAll(
                createDial(),
                createMinuteHand(),
                createHourHand(),
                createSecondsHand(),
                createTickMarks(),
                createCenter()
        );

時計盤

時計盤の枠は、javafx.scene.shape.Circleで描画します。枠の部分はグラデーションで凹凸を表現します。
UNIT_SIZEは、時計の半径の大きさです。

    // 時計の文字盤(背景)を作成する
    Node createDial() {
        RadialGradient gradient = new RadialGradient(
                0, 0, 0.5, 0.5, 0.5, true, CycleMethod.NO_CYCLE,
                new Stop(0.8, Color.WHITE), new Stop(0.9, Color.BLACK),
                new Stop(0.95, Color.WHITE), new Stop(1.0, Color.BLACK)
        );
        Circle circle = new Circle(UNIT_SIZE, UNIT_SIZE, UNIT_SIZE, gradient);
        return circle;
    }

時計盤の刻みは、0分から59分まで1分刻みでラインを回転させて作成します。
以下は、刻みを60個リストとして作成しjavafx.scene.Groupにまとめるコードです。

    // 時計の文字盤(分刻み)を作成する
    Node createTickMarks() {
        Group tickMarkGroup = new Group();
        List<Node> tickMarks = IntStream.range(0, 60)
                .mapToObj(this::createTickMark)
                .collect(toList());
        tickMarkGroup.getChildren().addAll(tickMarks);
        return tickMarkGroup;
    }

個々の分刻みは、javafx.scene.shape.Lineで作成します。分を引数として与え、5分ごとに長めの長さで、分の値に対応する角度だけ回転させて作成します。

    // 時計の文字盤の分刻みの1つを作成する
    Node createTickMark(int n) {
        Line line;
        if (n % 5 == 0) {
            line = new Line(UNIT_SIZE, UNIT_SIZE * 0.12, UNIT_SIZE, UNIT_SIZE * 0.2);
        } else {
            line = new Line(UNIT_SIZE, UNIT_SIZE * 0.15, UNIT_SIZE, UNIT_SIZE * 0.16);
        }
        line.getTransforms().add(new Rotate(360 / 60 * n, UNIT_SIZE, UNIT_SIZE));
        line.setStrokeWidth(2);
        return line;
    }

時計の針(短針、長針)

まず、短針・長針の共通の針の形状をjavafx.scene.shape.Pathで定義します。javafx.scene.shape.MoveToおよびjavafx.scene.shape.LineToで多角形を作成し、中を塗りつぶします。短針・長針は引数で長さ(stretchRelativeToRim)を与えています。

    // 時計の針を作成する
    Node createHourOrMinuteHand(double stretchRelativeToRim, Color color) {
        Path path = new Path(
                new MoveTo(UNIT_SIZE, UNIT_SIZE),
                new LineTo(UNIT_SIZE * 0.9, UNIT_SIZE * 0.9),
                new LineTo(UNIT_SIZE, stretchRelativeToRim),
                new LineTo(UNIT_SIZE * 1.1, UNIT_SIZE * 0.9),
                new LineTo(UNIT_SIZE, UNIT_SIZE)
        );
        path.setFill(color);
        path.setStroke(Color.TRANSPARENT);
        return path;
    }

短針を作成します。回転のためのjavafx.scene.transform.Rotateを設定します。回転の中心を針の根元とするため、パラメータにその位置を指定しています。

    // 時計の短針を作成する
    Node createHourHand() {
        hourHandRotation = new Rotate(0, UNIT_SIZE, UNIT_SIZE);
        Node hourHand = createHourOrMinuteHand(UNIT_SIZE * 0.4, Color.BLACK);
        hourHand.getTransforms().add(hourHandRotation);
        return hourHand;
    }

長針を作成します。

    // 時計の長針を作成する
    Node createMinuteHand() {
        minuteHandRotation = new Rotate(0, UNIT_SIZE, UNIT_SIZE);
        Node minuteHand = createHourOrMinuteHand(UNIT_SIZE * 0.2, Color.BLACK);
        minuteHand.getTransforms().add(minuteHandRotation);
        return minuteHand;
    }

秒針

秒針は、単純なLineとしています。

    // 時計の秒針を作成する
    Node createSecondsHand() {
        Line line = new Line(UNIT_SIZE, UNIT_SIZE * 1.1, UNIT_SIZE, UNIT_SIZE * 0.2);
        secondsHandRotation = new Rotate(0, UNIT_SIZE, UNIT_SIZE);
        line.getTransforms().add(secondsHandRotation);
        return line;
    }

回転

画像版で使用したRotateTransitionは、ノードの中心を回転中心としてしまいます。pivot(回転中心)を明示的に指定したRotateを与えたノードであっても同じ振る舞いをしています。そこで、Timelineを使ったアニメーションを実装します。

まず、秒針の回転を記述します。現在時刻を取得し、その時刻の針の位置(角度)を計算し、Timelineを生成します。

        LocalTime time = LocalTime.now();
        Timeline secondsAnimation = createRotateTimeline(
                Duration.seconds(60), getSecondsAngle(time), secondsHandRotation);
        secondsAnimation.play();
    Timeline createRotateTimeline(Duration duration, int startAngle, Rotate rotate) {
        Timeline timeline = new Timeline();
        rotate.setAngle(startAngle);
        timeline.getKeyFrames().add(
                new KeyFrame(duration, new KeyValue(rotate.angleProperty(), startAngle + 360))
        );
        timeline.setCycleCount(Animation.INDEFINITE);
        return timeline;
    }
    private int getSecondsAngle(LocalTime time) {
        return time.getSecond() * 360 / 60;
    }

AnalogClockSvg プログラムの構成

プログラムを格納したリポジトリは次です。

時計の構成

JavaFXのSVGPathで時計盤、針を定義していきます。

時計盤(枠)

SceneBuilderでGroupにSVGPathを貼り、そのコンテント属性に、時計盤の枠となる円を定義します。
200x200の大きさに収まる円を定義するので、半径100の円を作成します。
しかし、SVGのパス定義には直接円(楕円)を描くコマンドはありません。このサンプルはとにかくSVGで描こうという趣旨なので円弧を描くコマンドを使って少々強引に円を描きます。実際にはCircleシェープを使うのがよいでしょう。
円弧に対して、開始位置、終了位置、X軸方向の半径、Y軸方向の半径、円弧の傾き、長弧か短弧か、時計回りか反時計回りかを指定します。今回は、開始位置を円の頂上位置(100,0)、終了位置はぐるっと回って戻ってくる直前の位置(99,0)、半径はともに100、傾きなし、長弧を描き、時計回りに回ります。次の図に概念を示します。

svg_clockdial_circle-1.png

この図の概念をSVGパスデータで表現すると次のようになります。

M 100 0 A 100 100 0 1 1 99 0

M 100 0で、カレントポジションを座標(100, 0)に移動させ、
A 100 100 0 1 1 99 0で円弧(楕円弧)を描画します。
最初の100 100は、楕円弧の半径をX軸、Y軸ともに100を指定します。
続く0は、楕円弧のX軸の回転角度を0度(回転なし)を指定します。
次の1は、円弧の長弧(始点・終点を結ぶ)を指定します。
次の1は、時計回りに長弧を描きます。
最後の99 0は、終点を指定します。
始点はカレントポジション(100,0)です。始点と終点を同じ座標とすると何も描かれないので、始点の1画素左隣を終点としました。

svg_clockdial-1.png

次はグラデーションを指定します。

Fill属性をクリックして、RadialGradientを選択します。

svg_clockdial-2.png

stopポイントを複数作成します。

svg_clockdial-3.png

時計盤(分刻み)

直線を、時計盤の枠に沿って回転させて並べたいのですが、SVGパスデータでは回転を表現できないので、プログラムで回転させた座標を生成してそれを使用することにします。

JUnitのテストメソッドを1つ使って、SVGのパスデータを作成する処理を記述します。

public class AnalogClockControllerTest {
    @Test
    public void SVG() {
        String pathData = IntStream.range(0, 60)
                .mapToObj(this::createLine)
                .map(line -> String.format("M %.1f,%.1f L %.1f,%.1f", line.getStartX(), line.getStartY(), line.getEndX(), line.getEndY()))
                .collect(joining(" "));
        System.out.println(pathData);
    }
    private Line createLine(int minute) {
        Rotate rot = new Rotate(minute * 6, 100, 100);
        Point2D p0 = rot.transform(100, minute % 5 == 0 ? 12 : 15);
        Point2D p1 = rot.transform(100, minute % 5 == 0 ? 20 : 16);
        Line line = new Line(p0.getX(), p0.getY(), p1.getX(), p1.getY());
        return line;
    }
}

まず、0分から59分までの分の刻みをそれぞれLineとして作成します。createLineメソッドを設け、引数に分を取ります。まず0分の刻みのLineの始点終点を作成し、それを指定の分に応じた角度に回転させます。5分単位で長いLine、それ以外は短いLineとします。
各Lineごとに、作成したLineの始点へ移動するMコマンド、始点から終点へ線を描くLコマンドを文字列化し、それを60個結合した文字列を作ります。
ユニットテストを実行し、これを標準出力に出して、コピー&ペーストでSceneBuilderのSVGPathのContent属性欄に写します。

svg_clockdial-4.png

線の幅は、Stroke Widthプロパティに2を設定し、線の色はStrokeプロパティでColorを選択、黒色を指定しています。

時計の針(短針)

短針は、次のパスデータで作成します。

M 100,100 L 90,90 L 100,40 L 110,90 Z

塗りつぶすため、図形を閉じる「Z」コマンドを最後に指定しています。

svg_hourhand-1.png

SVGPathを貼るコンテナ(レイアウト)にStackPaneを使うと位置合わせがずれてしまうのでGroupに貼ります。

時計の針(長針)

長針は、次のパスデータで作成します。

M 100,100 L 90,90 L 100,20 L 110,90 Z

svg_minutehand-1.png

時計の針(秒針)

秒針は、次のパスデータで作成します。

M 100,110 L 100,20

svg_secondhand-1.png

時計の中心

時計の中心は、次のパスデータで作成します。

M 100,95 A 5,5 0 1 1 99 95

svg_center-1.png

回転

Timelineを使用した回転

Timelineを使用した回転は、AnalogClockDrawing版とほとんど同じです。

まず、コントローラクラスのinitalizeメソッドで、現在時刻を取得します。
次に表示中心位置(ここでは、(100,100))で回転するRotateインスタンスを生成します。
回転対象のSVGPathインスタンスにこのRotateインスタンスをセットします。
そして、針が一回転する時間、現在時刻における針の角度、RotateインスタンスをもとにTimelineインスタンスを作成します。コードを次に示します。

    public void initialize(URL url, ResourceBundle rb) {
        LocalTime now = LocalTime.now();

        Rotate secondHandRotate = new Rotate(0, 100, 100);
        secondHand.getTransforms().add(secondHandRotate);
        Timeline secondAnimation = createRotateTimeline(Duration.seconds(60), getSecondAngle(now), secondHandRotate);

同様に、分針、時針についてTimelineを生成します。
最後に生成したTimelineに対してplay()を呼んでアニメーションを開始します。

RotateTransitionを使用した回転

RotateTransitionは、回転対象ノードの中心を回転軸として回転します。SVGPathで定義したノードは、定義した座標を全て含む最少の矩形領域がLayoutBounds、BoundsInLocalに設定されます。初期はこの両者は同一です。回転中心を、(100, 100)にできれば時計の中心軸を回転軸とできるのですが、Timelineのようにnew Rotate(0, 100, 100)を作成しても振る舞いは変わりません(pivotを見てくれていない)。
そこで、平行移動をTranslateでさせて回転してから逆方向に平行移動してという処理を入れました。

    @FXML
    private SVGPath secondHand;

    public void initialize(URL url, ResourceBundle rb) {
        LocalTime now = LocalTime.now();

        RotateTransition secondTransition = createRotateTransition(
                Duration.seconds(60), secondHand, getSecondAngle(now), 360d, 100d, 100d
        );

回転中心の座標を任意に指定できるRotateTransitionを生成するメソッドを定義し、それを呼び出します。

    private RotateTransition createRotateTransition(
        Duration duration, Node node, double fromAngle, double byAngle, double pivotX, double pivotY
    ) {
        Bounds bounds = node.getBoundsInLocal();                               // (1)
        double defaultPivotX = (bounds.getMinX() + bounds.getMaxX()) / 2d;     // (1)
        double defaultPivotY = (bounds.getMinY() + bounds.getMaxY()) / 2d;     // (1)
        double translateX = defaultPivotX - pivotX;                            // (2)
        double translateY = defaultPivotY - pivotY;                            // (2)
        node.getTransforms().add(new Translate(translateX, translateY));       // (3)
        node.setTranslateX(-translateX);                                       // (4)
        node.setTranslateY(-translateY);                                       // (4)
        RotateTransition rt = new RotateTransition(duration, node);            // (5)
        rt.setFromAngle(fromAngle);                                            // (5)
        rt.setByAngle(byAngle);                                                // (5)
        rt.setCycleCount(Animation.INDEFINITE);                                // (5)
        rt.setInterpolator(Interpolator.LINEAR);                               // (5)
        return rt;
    }
  • (1) ノードのBoundsInLocalを取得し、その中心座標を計算します。
  • (2) 引数で指定した回転中心座標と、(1)の座標との差(ベクトル)を計算します。
  • (3) (2)の差を平行移動量としてノードのアフィン変換に加えます。
  • (4) 一連のアフィン変換後に平行移動する量を設定します。(3)の逆方向(正負反転)です。
  • (5) 通常にRotateTransitionを作成します。

ソースコードは、Gitのtopic_svgrotatetransitionブランチにあります。
source:github_analogclock|AnalogClockSvg@topic_svgrotatetransition

完成形

次のとおりです。

ウィンドウの外枠をなくす

昨今のデスクトッププログラム(ガジェットなど)は、OS(デスクトップ)標準のウィンドウ・タイトルバーと外枠がないタイプが多いです。そこで、タイトルバー、外枠を非表示にしてみます。

        Scene scene = new Scene(root, 200, 200, Color.TRANSPARENT);
        stage.initStyle(StageStyle.TRANSPARENT);

StageStyleで指定可能な定数
定数 意味
DECORATED プラットフォームの装飾(タイトルバーやウィンドウ枠)、背景を白色塗りつぶし
UNDECORATED プラットフォームの装飾(タイトルバーやウィンドウ枠)なし、背景を白色塗りつぶし
TRANSPARENT プラットフォームの装飾(タイトルバーやウィンドウ枠)なし、背景を透過
UNIFIED プラットフォームの装飾(タイトルバーやウィンドウ枠)、他
UTILITY プラットフォームの最小限の装飾(タイトルバーやウィンドウ枠)、背景を白色塗りつぶし

最初UNDECORATEDを指定したら、ウィンドウのタイトルバーと枠がなくなったものの、丸いアナログ時計の外側に四角くなるように白く塗りつぶされた表示が出てしまいました。ここは、TRANSPARENTが正解です。

なお、プラットフォームの装飾をなくしてしまうと、ドラッグ&ドロップでウィンドウを移動する、プログラムを終了する、といった操作ができなくなってしまいます。プログラム側で何らかの操作手段を実装する必要があります。

今回は、Sceneにマウスプレスイベントおよびマウスドラッグ終了イベントを拾わせ、その移動量でStageの画面上の位置を変更します。

public class AnalogClockSvg extends Application {

    private double dragStartX;
    private double dragStartY;

    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("AnalogClock.fxml"));

        Scene scene = new Scene(root, 200, 200, Color.TRANSPARENT);
        scene.setOnMousePressed(e -> {
            dragStartX = e.getSceneX();
            dragStartY = e.getSceneY();
        });
        scene.setOnMouseDragged(e -> {
            stage.setX(e.getScreenX() - dragStartX);
            stage.setY(e.getScreenY() - dragStartY);
        });
        stage.initStyle(StageStyle.TRANSPARENT);
        stage.setScene(scene);
        stage.show();
    }

その他機能

時計の大きさをマウスホイールで変える

時計の大きさは、ルートNodeのscaleに拡大/縮小率を指定することで変えます。
マウスのホイールで拡大/縮小することとします。マウスのホイールはSCROLLイベントで検知できます。

        scene.setOnScroll(e -> {
            double zoomFactor = e.getDeltaY() > 0 ? 1.1 : 0.9;
            root.setScaleX(root.getScaleX() * factor);
            root.setScaleY(root.getScaleY() * factor);
        });

これで、マウスホイールによって徐々に拡大または縮小します。

拡大 標準 縮小-1 縮小-2 縮小-3
scale_large-3.png scale_origin.png scale_small-3.png scale_small-6.png scale_small-9.png
タッチパネルでのドラッグ操作へ悪影響

さて、このプログラムをタッチパネル搭載マシンで実行し、タッチ操作でドラッグしたところ、時計の位置が移動せずに拡大・縮小動作を始めてしまいました。タッチパネル上でドラッグ操作をすると、SCROLLイベントが発生してしまい、マウスホイール操作用のsetOnScrollメソッドが呼ばれてしまいます。

Javadocでjavafx.scene.input.ScrollEventクラスのAPIドキュメントを調べてみました。
http://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/input/ScrollEvent.html

  • タッチ操作で発生したスクロールでは、touchCountが変化する都度、SCROLLイベントの前後にSCROLL_STARTEDとSCROLL_FINISHEDが発生する
  • マウスのホイールを回転させて発生したスクロールは、SCROLLイベントが単独で発生する

そこで、SCROLL_STARTEDに続くSCROLLイベントの場合は拡大・縮小処理をしないようにコードを修正します。

        scene.setOnScrollStarted(e -> isScrollStarted = true);
        scene.setOnScrollFinished(e ->isScrollStarted = false);
        scene.setOnScroll(e -> {
            if (isScrollStarted) return;
            double zoomFactor = e.getDeltaY() > 0 ? 1.1 : 0.9;
            root.setScaleX(root.getScaleX() * factor);
            root.setScaleY(root.getScaleY() * factor);
        });

時計の大きさをピンチ操作で変える

タッチパネルの操作では、通常大きさを変える操作をピンチイン/ピンチアウトで行います。
JavaFXでは、ZOOMイベントで取得することができます。

        scene.setOnZoom(e -> {
            root.setScaleX(root.getScaleX() * e.getZoomfactor());
            root.setScaleY(root.getScaleY() * e.getZoomfactor());
        });

発展・関連

JJUG CCC 2015 Springで「JavaFXグラフィックスとアニメーション入門」セッション

2015年4月11日(土)に開催されたJJUG CCC 2015 Springにおいて「JavaFXグラフィックスとアニメーション入門」というセッションでしゃべってきました。はてな日記に当日の様子を書きました。
http://d.hatena.ne.jp/torutk/20150412

簡単化のため、アニメーションはRotateTransitionだけに、FXMLも使わずJavaコードのみで実装しました。

analogclock.png 表示 (16.8 KB) 高橋 徹, 2014/08/30 20:13

analogclockimaging.swf (811 KB) 高橋 徹, 2014/08/31 16:56

svg_clockdial-1.png 表示 (82.7 KB) 高橋 徹, 2014/09/17 03:41

svg_clockdial-2.png 表示 (93.9 KB) 高橋 徹, 2014/09/17 03:41

svg_clockdial-3.png 表示 (36.9 KB) 高橋 徹, 2014/09/17 03:41

svg_clockdial_circle-1.png 表示 (6.42 KB) 高橋 徹, 2014/09/24 00:15

svg_clockdial-4.png 表示 (85.7 KB) 高橋 徹, 2014/09/24 07:22

svg_hourhand-1.png 表示 (90.5 KB) 高橋 徹, 2014/09/24 08:32

svg_minutehand-1.png 表示 (91 KB) 高橋 徹, 2014/09/24 08:32

svg_secondhand-1.png 表示 (91.2 KB) 高橋 徹, 2014/09/24 08:33

svg_center-1.png 表示 (91.2 KB) 高橋 徹, 2014/09/25 07:06

analogclockSvg-1.png 表示 (26.6 KB) 高橋 徹, 2014/09/26 21:18

analogclockSvg-2.png 表示 (76.7 KB) 高橋 徹, 2014/09/27 10:50

scale_origin.png 表示 (20.5 KB) 高橋 徹, 2015/02/15 23:16

scale_small-3.png 表示 (18.2 KB) 高橋 徹, 2015/02/15 23:16

scale_small-6.png 表示 (15.1 KB) 高橋 徹, 2015/02/15 23:16

scale_small-9.png 表示 (12.1 KB) 高橋 徹, 2015/02/15 23:16

scale_large-3.png 表示 (10.9 KB) 高橋 徹, 2015/02/15 23:16

クリップボードから画像を追加 (サイズの上限: 1 GB)