Transitioning Scenes in JavaFX

It took some messing around with JavaFX to get Scene transitions working in a sort of comfortable way for me. There are a few approaches published online, but is how I did it.

Talk is cheap, show me the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package com.example;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.event.Event;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Control;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.net.URL;
import java.util.Objects;
import java.util.Stack;

public class Main extends Application {
private static final Logger log = LogManager.getLogger();
public static Parent root;
public static Scene scene;
public static Stage stage;
public static Stack<URL> bypassSceneStack;


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

@Override
public void start(Stage primaryStage) {
try {
// start the UI
stage = primaryStage;
bypassSceneStack = new Stack<>();
stage.setTitle("My Application");
stage.getProperties().put("hostServices", this.getHostServices());
transitionScene(getClass().getResource("/ui/SignIn.fxml"));
fadeIn(root);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}

/**
* Overloaded Generic Transition Function to reduce the amount of code in the controllers.
* @param e Control event that was clicked to reference scene from
* @param fileName FXML resource to transition to
* @throws IOException
*/
public static void transitionScene(Event e, String fileName) throws IOException {
//derive the control that fired the transition
Control control = (Control)e.getSource();
transitionScene(control, fileName); // pass to the next overload
}

public static void transitionScene(String fileName) throws IOException {
URL resourceName = Main.class.getResource(fileName);
transitionScene(resourceName);
}

public static void transitionScene(Control control, String fileName) throws IOException {
log.debug("Sender: " + control.getId());
URL resourceName = Main.class.getResource(fileName);
transitionScene(resourceName); // pass to the main function
}

/**
* Generic Transition Function to simplify the controllers.
* This is for transitions that aren't fired by events.
* @param resourceName FXML resource to transition to
* @throws IOException
*/
public static void transitionScene(URL resourceName) throws IOException {
log.debug("Transitioning to: " + resourceName.toString());
Parent parent = FXMLLoader.load(resourceName);

// Loading FXML will trigger initialize on the associated controller.
// The controller may redirect the UI to a different FXML instead,
// so we now check to see if our bypass stack is empty or not.
if (!bypassSceneStack.isEmpty()) {
log.debug("Bypass scene set. Loading it instead: " + bypassSceneStack.peek());
parent = FXMLLoader.load(bypassSceneStack.peek());
bypassSceneStack.pop(); // pop it if we loaded it

// we might've loaded another redirect in that child FXML that we just loaded now,
// so peek again to capture that too.
// note: this won't cater for more that 2 levels of redirects
if (!bypassSceneStack.isEmpty()) {
log.debug("Another bypass scene set. Loading it instead: " + bypassSceneStack.peek());
parent = FXMLLoader.load(bypassSceneStack.peek());
bypassSceneStack.pop();
}
}
root = parent;
scene = new Scene(root);
log.debug("Configuring Scene");
stage.setScene(scene);
log.debug("Rendering Scene");
stage.show();
}

/**
* Fade in transition. Sets opacity of the Parent FXML from 100% to 0% over the
* supplied timeframe in milliseconds
* @param root Parent FXML to fade in
*/
private static void fadeIn(Parent root) {
log.debug("Fading In");
DoubleProperty opacity = root.opacityProperty();
Timeline fadeIn = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacity, 0.0)),
new KeyFrame(new Duration(1000), new KeyValue(opacity, 1.0))
);
fadeIn.play();
}
}

There’s a lot going on here, but fundamentally what I did was to create a stack of scenes to render, and just popping the top scene off the stack when the actual render would happen. The stack is static, so you can push to it from any controller.

The reason this became important was because I needed to handle cases where the initialize() function in a controller might perform some state check (such as isAuthenticated()), and then redirect the user to a different Scene instead. There’s no event fired after a scene has finished loading (an annoying oversight by the JavaFX team), so performing those checks and redirects got tricky.

You can see here in this controller, initialize does a check for the presence of an accessToken, and if it’s there pushes the HomePage onto the stack instead. The transitionScene method in Main will notice this newly pushed scene and load it instead:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FXML
public void initialize() {
try {
// check to see if we already have a token stored in config that we can authenticate with
String accessToken = Configuration.getInstance().getProperties().getAccessToken();
if (accessToken != null && !accessToken.equals("")) {
log.debug("Authentication Token Found");
// redirect to home page, using the bypass stack as we're still in initialize
Main.bypassSceneStack.push(getClass().getResource("/ui/HomePage.fxml"));
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}

Or with button events we can also trigger the transitions:

1
2
3
4
5
6
7
8
9
10
11
12
public void btnSignInClicked(ActionEvent actionEvent) {
try {
log.debug("Sign In Clicked");
// first, check if the configuration has a token or not
String accessToken = Configuration.getInstance().getProperties().getAccessToken();
if (accessToken != null && !accessToken.equals("")) {
log.debug("Existing Authentication Token Found");
// redirect to home page
Main.transitionScene(getClass().getResource("/ui/HomePage.fxml"));
} else {
log.debug("No Authentication Token Found, Starting Sign In");
...

There’s one caveat, which is mentioned in the code above: this approach will only handle 2 scenes pushed onto the stack. Any more than 2 redirects will just be ignored. This suited my use case, but if you need it to be smarter you could make it recursive instead.

You might also be wondering why I have so many transitionScene overloads. This was to accommodate the different ways in which scene transitions would occur - they weren’t always triggered by button events.

And finally, the fadeIn() function is just a bit of eye candy I added to make the application fade into view when launched. I also had a fadeOut function that did the inverse, and I used both for every scene transition, but I found that it was just too slow adding those delays in to every scene.