JavaFX Animation and Binding: Simple Countdown Timer

JavaFX has a powerful animation feature that’s flexible and easy to use. I’m going to build a simple countdown timer that uses animation to countdown a numeric display from 15 (for example) to zero. I can imagine such a timer in some sort of response game that limits the time you have to make a move (or answer a question). Our first version will not use binding, but since binding is such an important concept with JavaFX, I will also show you a version that includes binding. And, the approach that uses binding is the correct approach. But, first we’ll examine how to manually update the numeric display so we can concentrate on the timer code.

FXTimer Screen Shot

FXTimer at Start Up

Here is a screenshot of the application as it comes up. To start the timer, you click the Start Timer button. The numeric display then counts down—once per second—to zero. Anytime you click the Start Timer button, the timer resets to 15 and restarts the countdown. Listing 1 shows all of the code except the button’s event handler code (lines 46-51), which we’ll see later. Let’s look at the program structure and the graphical components first. JavaFX applications extend the Application class and override the start() method. The JavaFX runtime constructs the Application class instance and invokes your start() method. If you use the NetBeans IDE, the IDE will build a skeletal JavaFX application for you. You put your code inside method start(). (Of course, more involved applications will use additional classes defined in other Java files.) After the class declaration, we declare some class variables such as timeline (Timeline), timerLabel (Label), and timeSeconds (Integer). Inside method start(), we setup the scene graph. Defining the root node (a Group) and scene and setting the Stage’s title are part of the boilerplate code the NetBeans IDE provides. We then configure the Label (lines 38-41) and create and configure the Button (lines 43-51). Next, we use a vertical box layout component (VBox) so that the Label and Button are vertically aligned and we center the components horizontally (line 57). As you can see, you add nodes to the scene graph using grouping and layout components methods getChildren().add(node) (for adding a single child node) and getChildren().addAll(nodes) (for adding multiple children nodes). See lines 63 and 65 for examples of addAll() and add(). I’ve highlighted Line 39 to show you that we set the label’s text by converting the Integer timeSeconds to a String. We’ll then have to perform the same conversion to update the label’s text when the timer code decrements the counter.

package fxtimer;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;

public class FXTimer extends Application {

    // private class constant and some variables
    private static final Integer STARTTIME = 15;
    private Timeline timeline;
    private Label timerLabel = new Label();
    private Integer timeSeconds = STARTTIME;

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

    @Override
    public void start(Stage primaryStage) {

        // Setup the Stage and the Scene (the scene graph)
        primaryStage.setTitle("FX Timer");
        Group root = new Group();
        Scene scene = new Scene(root, 300, 250);

        // Configure the Label
        timerLabel.setText(timeSeconds.toString());
        timerLabel.setTextFill(Color.RED);
        timerLabel.setStyle("-fx-font-size: 4em;");

        // Create and configure the Button
        Button button = new Button();
        button.setText("Start Timer");
        button.setOnAction(new EventHandler() {

            public void handle(ActionEvent event) {
            // Button event handler code goes here . . .
            }
        });

        // Create and configure VBox
        // gap between components is 20
        VBox vb = new VBox(20);
        // center the components within VBox
        vb.setAlignment(Pos.CENTER);
        // Make it as wide as the application frame (scene)
        vb.setPrefWidth(scene.getWidth());
        // Move the VBox down a bit
        vb.setLayoutY(30);
        // Add the button and timerLabel to the VBox
        vb.getChildren().addAll(button, timerLabel);
        // Add the VBox to the root component
        root.getChildren().add(vb);

        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

Now for the animation part! Listing 2 shows the code for the Button’s event handler. When you press the Start Timer button, the runtime invokes the button’s handle() method. This handler sets up a timeline, which runs indefinitely and cycles once each second. Each time you press the button, the handler stops the timeline if it is active (lines 4-6). The button handler then resets the counter to its initial value (STARTTIME) (line 7), updates the numeric label to match the counter (line 10), and restarts the timeline (line 27). Okay, so what exactly is a timeline? A Timeline is a JavaFX object you use to define animations by specifying JavaFX object properties that change over time. So, if you want to move a Rectangle object horizontally, you specify different values for the Rectangle’s x-coordinate property at different times (in different keyframes). A timeline consists of one or more KeyFrames and timelines use these KeyFrame objects to represent the different time frames. Here, however, we’re defining a more specialized use case for a Timeline. After each cycle (when the KeyFrame time slot elapses), the KeyFrame’s own event handler is invoked. Now let’s look at the code that builds and initializes the Timeline object (lines 11-26). We want the timeline to run indefinitely until stopped (line 12). In this case, we create a single KeyFrame and give it a one second duration (Duration.seconds(1)). We then define an event handler for the KeyFrame, which is executed when the KeyFrame finishes. This means that after one second, the runtime invokes the code in the KeyFrame event handler (lines 17-25). So what does this KeyFrame event handler do? It decrements our counter. It updates the numeric label. And, if the counter is zero, it stops the timeline. (If the counter is not zero, the timeline restarts automatically because we told it to run indefinitely.)

button.setOnAction(new EventHandler() {  //Button event handler

    public void handle(ActionEvent event) {
        if (timeline != null) {
            timeline.stop();
        }
        timeSeconds = STARTTIME;

        // update timerLabel
        timerLabel.setText(timeSeconds.toString());
        timeline = new Timeline();
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.getKeyFrames().add(
                new KeyFrame(Duration.seconds(1),
                  new EventHandler() {
                    // KeyFrame event handler
                    public void handle(ActionEvent event) {
                        timeSeconds--;
                        // update timerLabel
                        timerLabel.setText(
                              timeSeconds.toString());
                        if (timeSeconds <= 0) {
                            timeline.stop();
                        }
                      }
                }));
        timeline.playFromStart();
    }
});

Implementing the Timer with Binding Note that to properly maintain the numeric label, we had to initialize the label (Listing 1, Line 39), reinitialize the label during the Start Button event handler (Listing 2, Line 10), and update the label in the KeyFrame’s event handler (Listing 2, Lines 20-21). A better approach is to formally declare that the label’s text property is dependent on variable timeSeconds. Whenever the value of timeSeconds changes, the timerLabel text also changes. This dependency is called binding. To implement binding, we change variable timeSeconds from Integer to IntegerProperty. Property types let you bind variables and are new with JavaFX 2.0. Here is the Property declaration for timeSeconds.

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
. . .

// Make timeSeconds a Property
private IntegerProperty timeSeconds =
        new SimpleIntegerProperty(STARTTIME);

Next, we bind the label’s text property to timeSeconds, as follows.

// Bind the timerLabel text property to the timeSeconds property
timerLabel.textProperty().bind(timeSeconds.asString());

This binding creates the desired dependency: the timerLabel label will now show the current value of timeSeconds. Note that we access the timerLabel text property with timerLabel.textProperty(). All JavaFX scene graph nodes use Property types to maintain property values. With binding in place, we can make several improvements. We remove the code that reinitializes and updates timerLabel. We also remove the KeyFrame’s event handler and instead animate timeSeconds directly. This means that we specify the ending value for timeSeconds (0) and the timeline will, over the duration of the keyframe, figure out the intermediate values and update timeSeconds accordingly. We no longer decrement timeSeconds manually and no longer play the timeline indefinitely. We make the duration 15 seconds. Here is the new, improved button event handler and timeline code. Since timeSeconds is now a Property type, we must use the setters and getters to access its value (line 7).

button.setOnAction(new EventHandler() {

    public void handle(ActionEvent event) {
        if (timeline != null) {
            timeline.stop();
        }
        timeSeconds.set(STARTTIME);
        timeline = new Timeline();
        timeline.getKeyFrames().add(
                new KeyFrame(Duration.seconds(STARTTIME+1),
                new KeyValue(timeSeconds, 0)));
        timeline.playFromStart();
    }
});

Download the FXTimer with Binding JavaFX source code here.

Thanks to Michael Heinrichs for his feedback on an earlier version of this post.