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.
/** * 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 */ publicstaticvoidtransitionScene(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 }
publicstaticvoidtransitionScene(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 */ publicstaticvoidtransitionScene(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 */ privatestaticvoidfadeIn(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 publicvoidinitialize(){ 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
publicvoidbtnSignInClicked(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.