Skip to content

Instantly share code, notes, and snippets.

@dalegaspi
Last active February 23, 2022 13:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dalegaspi/fe71c5f21213cff7097e1cbe641f58d1 to your computer and use it in GitHub Desktop.
Save dalegaspi/fe71c5f21213cff7097e1cbe641f58d1 to your computer and use it in GitHub Desktop.
Creating a Splash Screen with JavaFx

A Sample JavaFx Splash Screen Implementation

This is a relatively simple splash screen that uses CompletableFuture constructs and shows a strategy to show real initialization status by using callbacks. It took inspiration from this gist.

A YT video of this can be found here.

The basic idea on the callbacks is to have the data layer provide a Consumer or BiConsumer parameter so the Controller can apply updates on the screen depending on what's happening on the data layer. In this particular case, I am using vavr.io.Function3 because I wanted to callback accept 3 parameters, but the basic idea remains. The magic happens here:

Try.run(() -> catalog.loadImages((image, current, count) -> {
    Platform.runLater(() -> {
        buildInfo.setText("Build v" + ConfigurationManager.getVersionInfo().orElse("1.0"));
        statusText.setText(String.format("Loading %d of %d images...", current, count));
    });

    return null;
}));

This sets the statusText FXML field that's bound to the label that has the same fx:id field.

Note the use of Platform::runLater since the process itself can happen in another thread (or itself spawns more threads) and JavaFx only allows UI updates in its own thread.

Note that were using Google Guice here to create/inject the controller and all its dependencies. For more inforation on that see this StackOverflow post.

package my.app;
import com.google.inject.Guice;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.apache.commons.cli.*;
import org.librawfx.RAWImageLoaderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The main JavaFx entry point class
*/
public class AppFx extends Application {
private static Logger logger = LoggerFactory.getLogger(AppFx.class);
private Image getAppIcon() {
var appIconStream = getClass().getResourceAsStream("/logo.png");
return new Image(appIconStream);
}
@Override
public void start(Stage stage) {
var injector = Guice.createInjector(new JLiteBoxModule());
try {
// Load FXML
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/splash.fxml"));
// Set the controller and set the controller factory to use Guice
// see https://stackoverflow.com/a/23471463/918858
loader.setControllerFactory(injector::getInstance);
Parent root = loader.load();
Scene scene = new Scene(root, 600, 400);
stage.setTitle(App.APP_NAME);
stage.setScene(scene);
// remove borders
stage.initStyle(StageStyle.TRANSPARENT);
// show the splash screen
stage.show();
} catch (Exception e) {
logger.error("Exception: {}", e.getMessage(), e);
}
}
public static void main(String[] args) {
// do other initialization stuff here
launch(args);
}
}
package my.app
public class ImageCatalog {
public int loadImage(int index) {
// mock
Thread.sleep(250);
return index;
}
@Override
public boolean loadImages(Function3<String, Integer, Integer, Void> loadCallback) {
for (int idx = 0; idx < 100; i++) {
Try.of(() -> loadImage(idx))
.onSuccess(mi -> {
if (loadCallback != null) {
i -> loadCallback.apply(i.toString, counter.incrementAndGet(), names.size());
}
}))
.filter(Try::isSuccess)
.map(Try::get)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
}
return true;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<AnchorPane fx:id="mainSplashPane" prefHeight="400.0" prefWidth="600.0"
style="-fx-background-image: url('https://picsum.photos/600/400'); -fx-background-size: cover, auto;"
xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="my.app.SplashController">
<children>
<ImageView fitHeight="117.0" fitWidth="114.0" layoutX="243.0" layoutY="39.0" pickOnBounds="true"
preserveRatio="true">
<image>
<Image url="https://www.fillmurray.com/125/125"/>
</image>
</ImageView>
<Label blendMode="HARD_LIGHT" contentDisplay="CENTER" layoutX="196.0" layoutY="164.0" text="A Bill Murray App"
textAlignment="CENTER" textFill="#ffffffe5">
<font>
<Font name="SF Pro Text Bold" size="24.0"/>
</font>
</Label>
<Label fx:id="buildInfo" alignment="CENTER" blendMode="HARD_LIGHT" contentDisplay="CENTER" layoutX="175.0"
layoutY="200.0" prefHeight="18.0" prefWidth="251.0" text="Build v1.0" textAlignment="CENTER"
textFill="#ffffffe5">
<font>
<Font name="SF Pro Text Light" size="14.0"/>
</font>
</Label>
<Label fx:id="statusText" blendMode="HARD_LIGHT" layoutX="405.0" layoutY="371.0" opacity="0.7"
style="-fx-opacity: 60;" text="Loading images from catalog..." textAlignment="RIGHT"
textFill="#ffffffb3">
<font>
<Font name="SF Pro Text Light" size="12.0"/>
</font>
</Label>
</children>
</AnchorPane>
package my.app;
import com.google.inject.Injector;
import io.vavr.control.Try;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
public class SplashController implements Initializable {
private static Logger logger = LoggerFactory.getLogger(SplashController.class);
public AnchorPane mainSplashPane;
public Label statusText;
public Label buildInfo;
private ImageCatalog catalog;
private Injector injector;
@Inject
public SplashController(ImageCatalog catalog) {
this.catalog = catalog;
}
public void setInjector(Injector injector) {
this.injector = injector;
}
@Override
public void initialize(URL url, ResourceBundle rb) {
CompletableFuture.runAsync(() -> {
Try.run(() -> Thread.sleep(1000)); // this is just in case the data layer loads too quick.
Try.run(() -> catalog.loadImages((image, current, count) -> {
Platform.runLater(() -> {
buildInfo.setText("Build v" + ConfigurationManager.getVersionInfo().orElse("1.0"));
statusText.setText(String.format("Loading %d of %d images...", current, count));
});
return null;
}));
Platform.runLater(() -> {
try {
Stage stage = new Stage();
// Load FXML of the main app
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"));
// Set the controller and set the controller factory to use Guice
// see https://stackoverflow.com/a/23471463/918858
loader.setControllerFactory(injector::getInstance);
Parent root = null;
root = loader.load();
Scene scene = new Scene(root, 1200, 800);
stage.setScene(scene);
// show the main GUI
stage.show();
// hide the splash window
mainSplashPane.getScene().getWindow().hide();
} catch (IOException e) {
logger.error("Cannot load main screen: " + e.getMessage(), e);
}
});
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment