Countdown timer Java JavaFX

201 Views Asked by At

Can someone please help me make a countdown timer in JavaFX? I need the time to be taken from a label (the time is set by a slider) and I want to see the minutes and seconds remaining every second. The countdown should start when a button is pressed. I searched Stack Overflow and I tried to do it with ChatGPT, but I still have problems with it.


import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.IOException;
import java.net.URL;
import java.util.*;

public class MainController implements Initializable{

@FXML
private Button button;

@FXML
private Slider slider;

@FXML
private Label timerLabel;

@Override
public void initialize(URL url, ResourceBundle resourceBundle) {

//the initial value of the label is set with the slider

slider.valueProperty().addListener((ObservableValue<? extends Number> num, Number oldVal, Number newVal) -> {
            Integer value = Integer.valueOf(String.format("%.0f", newVal));
            timeLabel.setText(String.valueOf(value));

        });

}

//the action of the button

@FXML
private void start(){

//i tried another way and still does not work
timer = seconds;
Timer timerA = new Timer();
TimerTask task = new TimerTask() {
      @Override
      public void run() {
           if(timer >0){
                timeLabel.setText(String.valueOf(timer));
                timer--;
           }
           if(timer == -1){
                 timeLabel.setText(String.valueOf(timer));
                 timerA.cancel();
           }
        }
};
timerA.schedule(task,0,1000);
}
}

2

There are 2 best solutions below

12
VGR On

The message “timeline might not be initialized” means that you are trying to use timeline before it has been constructed. You are calling timeline.stop() inside an event handler which is being passed to the Timeline constructor; since the constructor hasn’t completed yet, the compiler cannot guarantee timeline has actually had a value assigned to it when your event handler runs. In other words, timeline might be initialized.

One way to address this is to add the KeyFrame separately. That way, the Timeline constructor has completely finished:

Timeline timeline = new Timeline();
// Construction complete;  now timeline can be used in an event handler.

timeline.getKeyFrames().add(new KeyFrame(Duration.seconds(1), event -> {
    // Decrease one second
    if (seconds > 0) {
        seconds--;
    } else {
        // If seconds reach zero, decrement minutes and set seconds to 59
        if (minutes > 0) {
            minutes--;
            seconds = 59;
        } else {
            // If minutes reach zero, decrement hours and set minutes and seconds to 59
            if (hours > 0) {
                hours--;
                minutes = 59;
                seconds = 59;
            } else {
                // If hours, minutes, and seconds reach zero, stop the timer
                hours = 0;
                minutes = 0;
                seconds = 0;
                timeline.stop();
            }
        }
    }
    // Update the label with the new values
    timerLabel.setText(String.format("Time Left: %02d:%02d:%02d", hours, minutes, seconds));
}));

Another way would be to declare timeline as a private field, rather than as a local variable. This works because, once the enclosing class has finished its own constructor, every field is guaranteed to be initialized. (If your code never assigned a value to a field, that field would automatically be initialized to null.)

3
James_D On

The immediate issue ("timeline might not be initialized") is answered by @VGR's answer. However, using a Timeline like this is simply not the way to create a countdown timer. The issue is that the timeline does not guarantee to call the event handler every second; it will merely be executed when the UI is rendered, if at least a second has passed.

When the countdown is started, you should compute the time at which it should end. Then "periodically" update the label by calculating the time remaining until the end time. You can do this part either with a timeline, or with an AnimationTimer (whose handle() method will be called each time the UI is due to be rendered). The AnimationTimer will add a little extra computation time at the expense of being as accurate as the JavaFX system will allow; here I think the computation is minimal enough that it is not an issue and would prefer that approach.

Here is the startComputation() method using this approach:

import javafx.animation.Animation;
import javafx.animation.AnimationTimer;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.time.Duration;
import java.time.LocalTime;

public class CountdownTimer extends Application {
    private int hours;
    private int minutes;
    private int seconds;

    private Label timerLabel = new Label();

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

    @Override
    public void start(Stage primaryStage) {
        VBox root = new VBox(10);
        root.setAlignment(Pos.CENTER);

        TextField hoursField = new TextField();
        hoursField.setPromptText("Hours");
        TextField minutesField = new TextField();
        minutesField.setPromptText("Minutes");
        TextField secondsField = new TextField();
        secondsField.setPromptText("Seconds");

        Button startButton = new Button("Start Timer");
        startButton.setOnAction(event -> {
            // Parse user input for hours, minutes, and seconds
            hours = Integer.parseInt(hoursField.getText());
            minutes = Integer.parseInt(minutesField.getText());
            seconds = Integer.parseInt(secondsField.getText());

            // Start the countdown timer
            startCountdown();
        });

        root.getChildren().addAll(hoursField, minutesField, secondsField,   startButton, timerLabel);

        Scene scene = new Scene(root, 300, 200);
        primaryStage.setTitle("Countdown Timer");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void startCountdown() {
        LocalTime end = LocalTime.now()
                .plusHours(hours)
                .plusMinutes(minutes)
                .plusSeconds(seconds);
        AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long l) {
                Duration remaining = Duration.between(LocalTime.now(), end);
                if (remaining.isPositive()) {
                    timerLabel.setText(format(remaining));
                } else {
                    timerLabel.setText(format(Duration.ZERO));
                    stop();
                }
            }

            private String format(Duration remaining) {
                return String.format("%02d:%02d:%02d",
                        remaining.toHoursPart(),
                        remaining.toMinutesPart(),
                        remaining.toSecondsPart()
                );
            }
        };

        timer.start();
    }
}

If you prefer a Timeline, replace the startCountdown() method with

    private void startCountdown() {
        LocalTime end = LocalTime.now()
                .plusHours(hours)
                .plusMinutes(minutes)
                .plusSeconds(seconds);
        Timeline timeline = new Timeline();
        timeline.getKeyFrames().add(new KeyFrame(Duration.seconds(1), e -> {
            java.time.Duration remaining = java.time.Duration.between(LocalTime.now(), end);
            if (remaining.isPositive()) {
                timerLabel.setText(format(remaining));
            } else {
                timerLabel.setText(format(java.time.Duration.ZERO));
                timeline.stop();
            }
        }));
        timeline.setCycleCount(Animation.INDEFINITE);
        timeline.play();
    }

    private String format(java.time.Duration remaining) {
        return String.format("%02d:%02d:%02d",
                remaining.toHoursPart(),
                remaining.toMinutesPart(),
                remaining.toSecondsPart()
        );
    }