For effect, I want a label to display time like a stop watch would. The time starts at 0, and ends somewhere around 2 or 3 seconds. The service is stopped when desired. The issue I am having, is because I am trying to update the text 1000 times per second, the timer is lagging behind.
Like I said, this is for effect and will be hidden as soon as the timer stops, but the time should be fairly accurate, not 3 seconds behind.
Is there any way I can make this faster? I would like to have all 4 decimal places if possible.
timer = new Label();
DoubleProperty time = new SimpleDoubleProperty(0.0);
timer.textProperty().bind(Bindings.createStringBinding(() -> {
return MessageFormat.format(gui.getResourceBundle().getString("ui.gui.derbydisplay.timer"), time.get());
}, time));
Service<Void> countUpService = new Service<Void>() {
@Override
protected Task<Void> createTask() {
return new Task<Void>() {
@Override
protected Void call() throws Exception {
while (!isCancelled()) {
Platform.runLater(() -> time.set(time.get() + 0.0001));
Thread.sleep(1);
}
return null;
}
};
}
};
The main reason your clock lags is because you increase the time by 0.0001 seconds (i.e. 1/10000 seconds) 1000 times per second. So every second it will increase by 0.1... But even if you fix that issue, it would still lag a small amount because you don't account for the time it takes to make the method calls. You can fix this by checking the system clock when you start, and then checking it every time you do the update.
Updating the label 1000 times per second is pretty much redundant, because JavaFX only aims to update the UI 60 times per second. (It will be slower if the UI thread is busy trying to do too much stuff, which you also make happen by scheduling so many calls to Platform.runLater()
.) You can fix this by using an AnimationTimer
. The AnimationTimer.handle(...)
method is called once every time a frame is rendered, so this effectively updates as often as JavaFX allows the UI to update.
AnimationTimer timer = new AnimationTimer() {
private long startTime ;
@Override
public void start() {
startTime = System.currentTimeMillis();
super.start();
}
@Override
public void handle(long timestamp) {
long now = System.currentTimeMillis();
time.set((now - startTime) / 1000.0);
}
};
You can start this with timer.start();
and stop it with timer.stop();
. Obviously you can add more functionality to set a time for it to run, etc, and call stop()
from the handle(...)
method if you need.
SSCEE:
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Timer extends Application {
@Override
public void start(Stage primaryStage) {
Label label = new Label();
DoubleProperty time = new SimpleDoubleProperty();
label.textProperty().bind(time.asString("%.3f seconds"));
BooleanProperty running = new SimpleBooleanProperty();
AnimationTimer timer = new AnimationTimer() {
private long startTime ;
@Override
public void start() {
startTime = System.currentTimeMillis();
running.set(true);
super.start();
}
@Override
public void stop() {
running.set(false);
super.stop();
}
@Override
public void handle(long timestamp) {
long now = System.currentTimeMillis();
time.set((now - startTime) / 1000.0);
}
};
Button startStop = new Button();
startStop.textProperty().bind(
Bindings.when(running)
.then("Stop")
.otherwise("Start"));
startStop.setOnAction(e -> {
if (running.get()) {
timer.stop();
} else {
timer.start();
}
});
VBox root = new VBox(10, label, startStop);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 320, 120);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The value passed into the handle(...)
method is actually a timestamp in nanoseconds. You could use this and set the startTime
to System.nanoTime()
, though the precision you get there is way more than you can realistically use when the frames render at maximum of 60 frames per second.