One of the best things about my job is that it give me the opportunity to write code and play with the latest technologies. As a JavaFX developer I've created all sort of demos, and last year I faced an issue many developer might be facing today: How to transition between screens and how to manage the screen's stack? That is the reason why I've decided to write about my experiences with it, and to share with you a small framework I wrote for that.
For JavaOne 2012 I decided to create a Casino demo, and I needed to have multiple screens, one for each game:
Creating the screens was the easiest part: I used NetBeans in conjunction with JavaFX Scene Builder.
From NetBeans you have multiple options:
You can create a New Project, and select JavaFX FXML Application. This option will generate a project containing three files: a fxml file, a java controller and a main class. The fxml file is where all your UI components will be define. The java controller class has the fxml elements injections along with some methods used by the UI. Finally, the main class loads the fxml file, and start the execution of your application.
Second option is from an existent project. You can right click your project and select Add Empty fxml file. You will be prompted to acknowledge the creation of a controller associated with this new fxml file, which I recommend to do!. Different from the previous option, in this case no main class will be created, and somewhere in your code you are in charge of loading and displaying your fxml elements.
Once you have the initial fxml file, you can edit it using the JavaFX Scene Builder. It's really easy to use, and you can find some tutorials about it here.
Now, things seem to be really easy, right? Well, not quite. As you start creating your screens, you find yourself with a bunch of fxml files and controllers, one per each screen. This seems reasonable, as we don't want to place all the screens in a single fxml file, but how to manage them?
Here are a few things to keep in mind:
StackPane seems to be the first option you can thing of, right? Just have all the screens stacked one on top of each other, and the one on top gets displayed. Changing screens will be just switching places in the stack. This is not a very good idea, as we might be encountering some performance issues. The scene graph will be huge, and loaded with a lot of UI components that are not even visible. Keeping your scene graph small is the first trick for a good performance in your JavaFX application.
We definitely want to keep separate fxml for each screen, designing and maintaining the screens will be a much easier job.
One controller per screen also makes sense. One more time, we want to maintain separate screens elements and behaviors.
We need to define clean and easy navigation among the screens.
- Once again, we need to keep our scene graph small.
Then, what should we do?
StackPane still is a great option for the container, we just need to manage it properly. In the proposed framework, we only have one screen stacked at a time. In our design, I opted for a common background, so the transition between screens was really smooth. When we are transitioning to a new screen, I play a couple of animations. As I've mentioned before I just used fade transitions, but you can customize your framework and have any transition you want. The background always stays there, and the first fade transition takes place, fading out the current screen. The framework implements an EventHandler, so we can listen when this fading process ends, as we need to remove the already invisible screen, and show the new one by adding it to the scene graph and playing a fade-in animation. Only one screen is uploaded in the scene graph. This allows us to keep it as light as possible, and we don't have a negative impact in performance. Here are the steps:
- Display main screen
- User select a game
The current screen starts to fade out. Once it finishes, the old screen gets remove from the scene graph and the new screen start to fade in.
- Now the new screen is totally visible (opacity 1), and our graph stays light.
- The StackPane finishes with only one screen in its tree.
With this approach, you can define any transition between the screens. You can drop the new screen from the top, or you can slide in the new screen from any side; you can use any animation you can think of.
Now, lets have a look at the code:
First, all the screens need to know about their parent, in our case the main screen, as we need to be able to return to the main menu once we finish playing, perhaps to choose a different game. A common interface will do the trick (ControlledScreen), with a method for parent injection (setScreenParent), so we can initialize each screen's parent.
public interface ControlledScreen {
//This method will allow the injection of the Parent ScreenPane
public void setScreenParent(ScreensController screenPage);
}For each screen, we keep a separate fxml file, and a controller java file, as we mentioned at the beginning of the blog. Each controller class should implement the ControlledScreen, so all of them shared the type, and we can set the screen's parent later on.
public class Screen1Controller implements Initializable,
ControlledScreen {
ScreensController myController;
@Override
public void initialize(URL url, ResourceBundle rb) {
// TODO
}
public void setScreenParent(ScreensController screenParent){
myController = screenParent;
}
//any required method here
}Now as each controller class has a reference to the parent screen, you can define methods to perform the correct navigation. For example if you want to go back to main screen from one of the games, you just need to call somewhere the following method.
@FXML
private void goToMain(ActionEvent event){
myController.setScreen(ScreensFramework.MAIN_SCREEN);
}
We need a new class (ScreensController) for managing the screens:
- This class will extend StackPane, as it seems to be a perfect choice for our scenario.
public class ScreensController extends StackPane { - ScreensController will have a HashMap
called screens. This collection contains pairs formed by the screen's ID and the node representing the top of the screen's scene graph.
private HashMap<String, Node>screens = new HashMap<>(); This class has methods for adding, loading and setting the screens:
- addScreen(String id, Node screen) add the pair (id, screen) to the HashMap screens.
public void addScreen(String name, Node screen) {
screens.put(name, screen);
} loadScreen(String id, String resource) This method loads the fxml file specified by resource, and it gets the top Node for the screen. We can also get the controller associated to this screen, which allows us to set the parent for the screen, as all the controllers shared the common type ControlledScreen. Finally the screen is added to the screens hash map. As you can see from the code, the loaded fxml file, doesn't get added to the scene graph, so the loaded screen doesn't get displayed or loaded to the screen.
public boolean loadScreen(String name, String resource) {
try {
FXMLLoader myLoader = new
FXMLLoader(getClass().getResource(resource));
Parent loadScreen = (Parent) myLoader.load();
ControlledScreen myScreenControler =
((ControlledScreen) myLoader.getController());
myScreenControler.setScreenParent(this);
addScreen(name, loadScreen);
return true;
}catch(Exception e) {
System.out.println(e.getMessage());
return false;
}
}setScreen(String screenName). This method displays a screen with a given screen name.
First the method verifies that the screen has been previously loaded.
We check if there is already a screen been displayed, so we need to play the screen transition sequence. If there isn't any screen, we just add it to the graph and perform a nice fade-in animation.
If there is a screen already been displayed, we play an animation to fade out the current screen, and we defined an eventHandler to handle execution after this.
Once the screen is invisible, we remove it from the graph, and we add the new screen. Again, a nice animation is played to show the new screen.
public boolean setScreen(final String name) {
if(screens.get(name) != null) { //screen loaded
final DoubleProperty opacity = opacityProperty();
//Is there is more than one screen
if(!getChildren().isEmpty()){
Timeline fade = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(opacity,1.0)),
new KeyFrame(new Duration(1000),
new EventHandler() {
@Override
public void handle(ActionEvent t) {
//remove displayed screen
getChildren().remove(0);
//add new screen
getChildren().add(0, screens.get(name));
Timeline fadeIn = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(opacity, 0.0)),
new KeyFrame(new Duration(800),
new KeyValue(opacity, 1.0)));
fadeIn.play();
}
}, new KeyValue(opacity, 0.0)));
fade.play();
} else {
//no one else been displayed, then just show
setOpacity(0.0);
getChildren().add(screens.get(name));
Timeline fadeIn = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(opacity, 0.0)),
new KeyFrame(new Duration(2500),
new KeyValue(opacity, 1.0)));
fadeIn.play();
}
return true;
} else {
System.out.println("screen hasn't been loaded!\n");
return false;
}
- addScreen(String id, Node screen) add the pair (id, screen) to the HashMap screens.
- We also have an unloadScreen(String name) method, that simple removes the screen from our hashmap, and report the status of this operation.
public boolean unloadScreen(String name) {
if(screens.remove(name) == null) {
System.out.println("Screen didn't exist");
return false;
} else {
return true;
}
}
- This class will extend StackPane, as it seems to be a perfect choice for our scenario.
Now all we have to do is start using the framework. Here I show a small piece of code where you load the screens, and show the main one.
public class ScreensFramework extends Application {
public static final String MAIN_SCREEN = "main";
public static final String MAIN_SCREEN_FXML = "main.fxml";
public static final String POKER_SCREEN = "poker";
public static final String POKER_SCREEN_FXML =
"poker.fxml";
public static final String ROULETTE_SCREEN = "roulette";
public static final String ROULETTE_SCREEN_FXML =
"roulette.fxml";
@Override
public void start(Stage primaryStage) {
ScreensController mainContainer = new ScreensController();
mainContainer.loadScreen(ScreensFramework.MAIN_SCREEN,
ScreensFramework.MAIN_SCREEN_FXML);
mainContainer.loadScreen(ScreensFramework.POKER_SCREEN,
ScreensFramework.POKER_SCREEN_FXML);
mainContainer.loadScreen(ScreensFramework.ROULETTE_SCREEN,
ScreensFramework.ROULETTE_SCREEN_FXML);
mainContainer.setScreen(ScreensFramework.MAIN_SCREEN);
Group root = new Group();
root.getChildren().addAll(mainContainer);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
...
I found this framework very useful for my application, and I hope this will be valuable for you too. You can find a full implementation with three testing screens available following this link.
You can also see a related video I've created on our YouTube Java Channel, so you can see this application live.