(English version)
Una de las mejores cosas de mi trabajo es que tengo la oportunidad de escribir código y jugar con las últimas technologias. He sido desarrolladora de JavaFX ya por varios años y he creado todo tipo de demostraciones. El año pasado me encontré con un problema que algunos de ustedes pueden de estar teniendo hoy en dia: Como navigar entre pantallas y como manejar fácilmente el stack the pantallas en nuestro scene graph? Esta es la razón por al cual me he decidido a escribir este blog y compartir con ustedes este pequeno framework que escribi para mi aplicacion.
Para JavaOne 2012 decidí crear una aplicacion para un casino, y necesitaría una pantalla diferente para cada juego:
Crear las pantallas fue la parte mas fácil: Utilizé NetBeans y JavaFX Scene Builder.
Para NetBeans hay varias opciones:
Puedes crear New Project, y seleccionar JavaFX FXML Application. Esta opción genera un projecto que contiene 3 archivos: un archivo fxml, una clase controlladora, y una clase main. El archivo fxml es donde todos los componentes the la interfaz gráfica con definidos. El controlador, tiene la injeccion de los elementos fxml junto con algunos métodos utilizados por dicha interfaz gráfica. Finalmente, la clase main carga el archivo fxml, y comienza la ejecucion de la applicación.
La segunda opición es a partir de un projecto existente. Puedes hacer click derecho en el projecto y seleccionar Add Empty fxml file. Una pantalla te preguntara si deseas la creación del controllador asociado con el nuevo archivo fxml, en mi opinion, si se debe de crear esta clase controladora y mas tarde en este blog miraremos por que. A diferencia de la opción anterior, en este caso no se crea una clase main, y en algún lugar de nuestro código estamos a cargo de cargar y desplegar los componentes gráficos creados en el archivo fxml.
A partir del fxml inicial, podemos editar fácilmente este archivo usando JavaFX Scene Builder. Es verdaderamente fácil de usar, y puedes encontrar tutoriales hacerca de esto aqui.
Ahora, todo parece muy simple, verdad? Bueno, no tanto. A medida de que vamos creando las pantallas, nos encontraremos que tenemos un montón de archivos fxml y un montón de clases controladoras, una por cada pantalla creada. Esto parece rasonable, ya que no queremos por ningún motivo tener la definición de todas las pantallas en un solo archivo fxml. Pero como manejarlas?
Miremos que debemos de tener en cuenta:
Lo primero que se nos viene a la cabeza es utilizar un StackPane, correcto? Ponemos todas las pantallas en este Stack, una encima de la otra, y la que este en el tope del Stack es la que sería desplegada. Para cambiar de pantalla, sería tan fácil como cambiar de posición las pantallas en dicho Stack. Esto, aunque parece simple, no sería buena idea, ya que el rendimiento de la aplicación se veria seriamente afectado. El scene graph sería inmenso, y cargado de componentes gráficos que no son nisiquiera visibles. Uno de las primeras reglas en JavaFX para tener buen rendimiento es mantener el scene graph pequeño.
Definitivamente deseamos mantener archivos fxml separados, uno por cada pantalla. El diseño y el mantenimiento de estos serán muchisimo mas fácil de esta manera.
También tiene sentido tener un controlador por cada pantalla. De nuevo, queremos mantener separados no sólo los componentes gráficos sino tambien los comportamientos asociados a estos.
Necesitamos definir una navegacion limpia y fácil entre las pantallas.
- Una vez más, MANTIENE el scene graph pequeño!
Entonces, que podríamos hacer?
StackPane sigue siendo una opción excelente como contenedor, simplemente se debe manejar de forma apropiada. En el framework que you cree, el stack pane sólo posee una pantalla en el stack al tiempo. Para mi aplicación yo opté por una imagen común como fondo de la aplicación, asi las transiciones entre pantallas serían mas fáciles. Para pasar a una nueva pantalla, una serie de animaciones tienen lugar. Por simplicidad de este framework utilicé "fade transitions", pero puedes cambiar y utilizar la transición que desees.
La imagen de fondo de la applicación siempre permanece en el scene graph, y es visible todo el tiempo. Para movernos a una nueva pantalla, la primera transicion 'fade' tiene lugar, haciendo desaparecer los componentes graficos de la pantalla actual. El framework utiliza un EventHandler, el cual será notificado cuando esta transición 'fade' termine. Cuando esto sucede, pasamos a retirar los components de la pantalla que estaba siendo desplegada y que en este momento ya son invisibles. Luego adicionamos los componentes gráphicos de la nueva pantalla (pantalla a ser desplegada) que por defecto son invisibles, pasando luego a ejecutar una nueva transicion en la que hacemos visibles todos sus componentes. De esta manera, solo una pantalla esta cargada en el scene graph y no se vera afectada el buen rendimiento de la aplicación. Aqui estan los pasos a seguir:- Mostrar la pantalla principal
- El usuario selecciona un juego
La pantalla actual empieza a desaparecer (transición). Al terminar, esta es retirada el scene graph y la nueva pantalla comienza a hacerce visible.
- Hasta que finalmente la nueva pantalla es completamente visible.
- El StackPane termina tan sólo con una pantalla en su scene graph.
Con este método, se puede especificar cualquier transicion deseada entre pantallas. Estas pueden entrar de arriba hacia abajo, or entrar horizontalmente por uno de los lados: puedes implementar la animación que desees.
Ahora miremos el código:
Primero, todas las pantallas necesitan saber sobre su padre, en nuestro caso fue la pantalla main, ya que necesitamos poder regresar al menu principal una vez el usuario termine de jugar, o simplemente porque el usuario desee seleccionar un nuevo juego. Para esto necesitaremos una interfaz común (ControlledScreen), con un método para la injección del padre(setScreenParent).
public interface ControlledScreen {
//This method will allow the injection of the Parent ScreenPane
public void setScreenParent(ScreensController screenPage);
}Por cada pantalla mantenemos un archivo fxml separado, al igual que una clase controladora, como lo mensionamos al principio de este blog. Cada controlador debe implementar ControlledScreen, para que todos ellos compartan el mismo tipo, y podamos mas tarde asociara el padre a cada pantalla.
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
}Ahora que cada controlador tiene la referencia de su padre, podemos pasar a definir los métodos que realizarán la navegación. Por ejemplo, si deseamos regresar a la pantalla principal de uno de los juegos, debemos ejecutar el siguiente método:
@FXML
private void goToMain(ActionEvent event){
myController.setScreen(ScreensFramework.MAIN_SCREEN);
}
Necesitamos una nueva clase (ScreensController) para majenar las pantallas:
- Esta clase de heredar StackPane, ya que parece ser la opción mas adecuada para nuestro escenario.
public class ScreensController extends StackPane { - ScreensController contiene un HashMap
llamado screens. Esta collección contiene parejas construídas por el identificador de la pantalla junto al nodo que representa la raiz de su scene graph.
private HashMap<String, Node>screens = new HashMap<>(); Esta clase debe definir métodos para addicionar, cargar y mostrar la pantalla adecuada:
- addScreen(String id, Node screen) adiciona una pareja (id, screen) al HashMap screens.
public void addScreen(String name, Node screen) {
screens.put(name, screen);
} loadScreen(String id, String resource) Este método carga el archivo fxml especificado por resource, y obtiene el nodo raiz para you pantalla. También podemos obtener el controlador asociado con la pantalla, esto nos permitirá configurar el padre. Esto es posible ya que todos los controladores comparten el mismo tipo ControlledScreen.
Finalmente, la pantalla es adicionada al hash map llamado screens. Como se puede observar en el código, el archivo fxml que se ha cargado, aun no se ha adicionado al scene graph, osea que aun no es desplegado por JavaFX.
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). Este método muestra la pantalla especificada con el identificador dado.
Primero verificamos si la panatalla reconocida por ese identificador ha sido cargada previamente.
También tenemos que chequear si hay ya una pantalla desplegada (necesitamos entonces hacer transiciones entre pantallas), o si esta es la primera pantalla a ser mostrada (simplemente se muestra la pantalla).
Si ya existe una pantalla, ejecutamos la transicion y definimos el eventHandler para que se haga cargo de la ejecución, una vez termine esta transición.
Una vez la antigua pantalla se hace invisible, se remueve del scene graph, y se adiciona la nueva. De nuevo, una animación es realizada para mostrar la nueva pantalla.
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) adiciona una pareja (id, screen) al HashMap screens.
- Tambien necesitamos un método unloadScreen(String name). Este simplemente remueve la pantalla de nuestra hash map, reportando el status de esta operación.
public boolean unloadScreen(String name) {
if(screens.remove(name) == null) {
System.out.println("Screen didn't exist");
return false;
} else {
return true;
}
}
- Esta clase de heredar StackPane, ya que parece ser la opción mas adecuada para nuestro escenario.
Ahora lo único que necesitamos es comenzar a utilizar el framework. Aqui esta una pequeña parte de código que muestra como hacerlo.
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();
}
...
Este framework fue de bastante ayuda para la creación de mi aplicación y espero que también sea de utilidad para usteded. Pueden encontrar la implementación de este framework, junto con tres clases para verificar que todo funcione en este link.
Tambien hay un video asociado a este blog que pueden encontrar aqui.