Java FX 17 Analog Clock Example

In this tutorial we will see how to make Java FX analog clock. we have Main class which Create the Circle and the hands (for hour minute and second). Then we use HandTransitions to add the angle transitions related to the hands. Back to the Main class we create VBox layout and as a main container and add all the competent. Then we create the scene and clock.css styles. At the end we use MouseActions class to setup double click action and also drag and drop.

You can find the Github repo of this project here

First let’s see the project structure:

Then let’s check out the main class Main:

package com.example.clock;

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class Main extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    public void start(final Stage stage) {
        // construct the analogueClock pieces.
        final Circle face = new Circle(100, 100, 100);
        final Line hourHand = new Line(0, 0, 0, -50);
        hourHand.setTranslateX(100);
        hourHand.setTranslateY(100);
        hourHand.setId("hourHand");
        final Line minuteHand = new Line(0, 0, 0, -75);
        minuteHand.setTranslateX(100);
        minuteHand.setTranslateY(100);
        minuteHand.setId("minuteHand");
        final Line secondHand = new Line(0, 15, 0, -88);
        secondHand.setTranslateX(100);
        secondHand.setTranslateY(100);
        secondHand.setId("secondHand");

        face.setFill(Color.TRANSPARENT);
        final Group analogueClock = new Group(face, hourHand, minuteHand, secondHand);

        HandTransitions.create(hourHand, minuteHand, secondHand);

        stage.initStyle(StageStyle.TRANSPARENT);

        // layout the scene.
        final VBox layout = new VBox();
        layout.getChildren().addAll(analogueClock);
        layout.setAlignment(Pos.CENTER);
        layout.setMinSize(300, 300);
        final Scene scene = new Scene(layout, Color.TRANSPARENT);
        scene.getStylesheets()
                .add(Main.class.getResource("clock.css").toString());
        stage.setScene(scene);

        // Add mouse actions. Drag and drop and style change of double click
        MouseActions mouseActions = new MouseActions();
        mouseActions.init(stage, scene, layout);

        // Get the application icon
        stage.getIcons()
                .add(new Image(Main.class.getResourceAsStream("icon.png")));
        // show the scene.
        stage.show();
    }

}

HandTransitions add the angle transitions related to the hands. Checking HandTransitions we can see that for initial values of this angles we use the previous calculated initial hands angels. For example the seedHourDegrees is using seedMinuteDegrees and seedSecondDegrees. Then we set the rotation for each hand. For example the Second Hand must rotate a whole cycle for 60 seconds.

package com.example.clock;

import javafx.animation.*;
import javafx.scene.shape.Line;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;

import java.time.LocalDateTime;

public class HandTransitions {

    public static void create(Line hourHand, Line minuteHand, Line secondHand) {
        // determine the starting time.
        LocalDateTime localDateTime = LocalDateTime.now();

        final double seedSecondDegrees = localDateTime.getSecond() * (360.0 / 60);
        final double seedMinuteDegrees =
                (localDateTime.getMinute() + seedSecondDegrees / 360.0) * (360.0 / 60);
        final double seedHourDegrees =
                (localDateTime.getHour() + seedMinuteDegrees / 360.0) * (360.0 / 12);

        // define rotations to map the analogueClock to the current time.
        final Rotate hourRotate = new Rotate(seedHourDegrees);
        final Rotate minuteRotate = new Rotate(seedMinuteDegrees);
        final Rotate secondRotate = new Rotate(seedSecondDegrees);
        hourHand.getTransforms().add(hourRotate);
        minuteHand.getTransforms().add(minuteRotate);
        secondHand.getTransforms().add(secondRotate);

        // the hour hand rotates twice a day.
        final Timeline hourTime = new Timeline(
                new KeyFrame(
                        Duration.hours(12),
                        new KeyValue(
                                hourRotate.angleProperty(),
                                360 + seedHourDegrees,
                                Interpolator.LINEAR
                        )
                )
        );

        // the minute hand rotates once an hour.
        final Timeline minuteTime = new Timeline(
                new KeyFrame(
                        Duration.minutes(60),
                        new KeyValue(
                                minuteRotate.angleProperty(),
                                360 + seedMinuteDegrees,
                                Interpolator.LINEAR
                        )
                )
        );

        // move second hand rotates once a minute.
        final Timeline secondTime = new Timeline(
                new KeyFrame(
                        Duration.seconds(60),
                        new KeyValue(
                                secondRotate.angleProperty(),
                                360 + seedSecondDegrees,
                                Interpolator.LINEAR
                        )
                )
        );

        // animation of time never ends.
        hourTime.setCycleCount(Animation.INDEFINITE);
        minuteTime.setCycleCount(Animation.INDEFINITE);
        secondTime.setCycleCount(Animation.INDEFINITE);

        secondTime.play();
        minuteTime.play();
        hourTime.play();
    }
}

We use MouseActions class to Load three different backgrounds and add action listener on double click which will cycle through them. We also save them with the help of Java Preferences class. Also in MouseActions we add drag and drop functionality.

package com.example.clock;

import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.Stage;

import java.util.concurrent.atomic.AtomicReference;

import java.util.prefs.*;

public class MouseActions {

    // Retrieve the user preference node for the package com.example.clock
    private static final Preferences prefs =
            Preferences.userNodeForPackage(com.example.clock.MouseActions.class);

    // Preference key name
    private static final String PREF_NAME = "clock_face";

    private AtomicReference<Integer> number = new AtomicReference<>(0);

    public void init(Stage stage, Scene scene, VBox layout) {

        Image clockImg1 =
                new Image(Main.class.getResource("1.png").toString(), 200, 200, true, true);
        BackgroundImage backgroundImage1 = new BackgroundImage(clockImg1,
                BackgroundRepeat.NO_REPEAT,
                BackgroundRepeat.NO_REPEAT,
                BackgroundPosition.CENTER,
                BackgroundSize.DEFAULT);

        Image clockImg2 =
                new Image(Main.class.getResource("2.png").toString(), 200, 200, true, true);
        BackgroundImage backgroundImage2 = new BackgroundImage(clockImg2,
                BackgroundRepeat.NO_REPEAT,
                BackgroundRepeat.NO_REPEAT,
                BackgroundPosition.CENTER,
                BackgroundSize.DEFAULT);

        Image clockImg3 =
                new Image(Main.class.getResource("3.png").toString(), 200, 200, true, true);
        BackgroundImage backgroundImage3 = new BackgroundImage(clockImg3,
                BackgroundRepeat.NO_REPEAT,
                BackgroundRepeat.NO_REPEAT,
                BackgroundPosition.CENTER,
                BackgroundSize.DEFAULT);

        extracted(layout, backgroundImage1, backgroundImage2, backgroundImage3);

        // allow the clock background to be used to drag the clock around.
        final Delta dragDelta = new Delta();
        layout.setOnMousePressed(mouseEvent -> {
            // record a delta distance for the drag and drop operation.
            dragDelta.x = stage.getX() - mouseEvent.getScreenX();
            dragDelta.y = stage.getY() - mouseEvent.getScreenY();
            scene.setCursor(Cursor.MOVE);

            // double click will change the clock look
            if (mouseEvent.getButton().equals(MouseButton.PRIMARY)) {
                if (mouseEvent.getClickCount() == 2) {

                    number.getAndSet(number.get() + 1);

                    saveUserSettings(number.get());
                    extracted(layout, backgroundImage1, backgroundImage2, backgroundImage3);
                }
            }
        });
        layout.setOnMouseReleased(mouseEvent -> scene.setCursor(Cursor.HAND));
        layout.setOnMouseDragged(mouseEvent -> {
            stage.setX(mouseEvent.getScreenX() + dragDelta.x);
            stage.setY(mouseEvent.getScreenY() + dragDelta.y);
        });
    }

    private void extracted(VBox layout,
                           BackgroundImage backgroundImage1,
                           BackgroundImage backgroundImage2,
                           BackgroundImage backgroundImage3) {
        Integer clockFace = getUserSettings();
        switch (clockFace) {
            case 1 -> layout.setBackground(new Background(backgroundImage1));
            case 2 -> layout.setBackground(new Background(backgroundImage2));
            case 3 -> layout.setBackground(new Background(backgroundImage3));
            default -> number.getAndSet(0);
        }
    }


    private void saveUserSettings(Integer number) {
        // Set the value of the preference
        prefs.put(PREF_NAME, number.toString());
    }

    private Integer getUserSettings() {
        // Get the value of the preference;
        // default value is returned if the preference does not exist
        String defaultValue = "1";
        String propertyValue = prefs.get(PREF_NAME, defaultValue);
        return Integer.valueOf(propertyValue);
    }

    // records relative x and y co-ordinates.
    static class Delta {
        double x, y;
    }
}

Then lets check module-info.java file:

module com.example.clock {
    requires javafx.controls;
    requires javafx.fxml;
    requires java.prefs;


    opens com.example.clock to javafx.fxml;
    exports com.example.clock;
}

and the pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation=
                 "http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>Clock</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>Clock</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <junit.version>5.9.1</junit.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>17.0.7</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>17.0.7</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-maven-plugin</artifactId>
                <version>0.0.8</version>
                <executions>
                    <execution>
                        <!-- Default configuration for running with: mvn clean javafx:run -->
                        <id>default-cli</id>
                        <configuration>
                            <mainClass>com.example.clock/com.example.clock.Main</mainClass>
                            <launcher>start</launcher>
                            <jlinkImageName>clock</jlinkImageName>
                            <compress>2</compress>
                            <noManPages>true</noManPages>
                            <stripDebug>true</stripDebug>
                            <noHeaderFiles>true</noHeaderFiles>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Leave a Comment

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.