I've created a busy layer showing an animating progress indicator while background IO is busy. The layer is added to the glasspane in Gluon mobile 4:
BusyLayer extends Layer { ...
root = new FlowPane(new ProgressIndicator());
MobileApplication.getInstance().getGlassPane().getLayers().add(this);
DH2FX extends MobileApplication { ...
addLayerFactory("Busy", () -> new BusyLayer());
...
showLayer("Busy");
...
hideLayer("Busy");
In Gluon 5 getLayers has been removed and according to the migration guide layers can be shown directly:
BusyLayer extends Layer { ...
root = new FlowPane(new ProgressIndicator());
DH2FX extends MobileApplication { ...
BusyLayer busyLayer = new BusyLayer();
...
busyLayer.show();
...
busyLayer.hide();
But the layer is not hidden.
====
The main players are a singleton Background class, so the BusyLayer is only shown once:
class BackgroundActivity {
private final AtomicInteger busyAtomicInteger = new AtomicInteger(0);
BusyLayer busyLayer = new BusyLayer();
private long time;
public BackgroundActivity() {
busyLayer.setOnShowing(e -> {
time = System.currentTimeMillis();
System.out.println("Showing busyLayer");
});
busyLayer.setOnShown(e -> {
System.out.println("busyLayer shown in: " + (System.currentTimeMillis() - time) + " ms");
});
busyLayer.setOnHiding(e -> System.out.println("hiding layer at " + (System.currentTimeMillis() - time) + " ms"));
}
void start() {
if (busyAtomicInteger.getAndIncrement() == 0) {
busyLayer.show();
}
}
void done() {
if (busyAtomicInteger.decrementAndGet() == 0) {
busyLayer.hide();
}
}
void failure(Throwable t) {
t.printStackTrace();
failure();
}
void failure() {
done();
}
}
protected final BackgroundActivity backgroundActivity = new BackgroundActivity();
And code like this using CompletableFutures to do asynchronous tasks:
// Hours
backgroundActivity.start();
CompletableFuture.supplyAsync( () -> entryService().getHours(calendarPickerForHessian))
.exceptionally( e -> { backgroundActivity.failure(e); return null; } )
.thenAcceptAsync( (Hour[] hours) -> {
Platform.runLater( () -> {
refreshHours(hours);
backgroundActivity.done();
});
});
// ProjectTotals
backgroundActivity.start();
CompletableFuture.supplyAsync( () -> entryService().getProjectTotals(calendarPickerForHessian) )
.exceptionally( e -> { backgroundActivity.failure(e); return null; } )
.thenAcceptAsync( (LinkedHashMap<Integer, Double> projectTotals) -> {
Platform.runLater( () -> {
refreshProjectTotals(projectTotals);
backgroundActivity.done();
});
});
// DayTotals
backgroundActivity.start();
CompletableFuture.supplyAsync( () -> entryService().getDayTotals(calendarPickerForHessian))
.exceptionally( e -> { backgroundActivity.failure(e); return null; } )
.thenAcceptAsync( (SortedMap<String, Double> dayTotals) -> {
Platform.runLater( () -> {
refreshDayTotals(dayTotals);
backgroundActivity.done();
});
});
And ofcourse BusyLayer itself:
public class BusyLayer extends Layer {
public BusyLayer() {
root = new StackPane(new ProgressIndicator());
root.setAlignment(Pos.CENTER);
root.getStyleClass().add("semitransparent7");
getChildren().add(root);
}
private final StackPane root;
@Override
public void layoutChildren() {
root.setVisible(isShowing());
if (!isShowing()) {
return;
}
GlassPane glassPane = MobileApplication.getInstance().getGlassPane();
root.resize(glassPane.getWidth(), glassPane.getHeight());
resizeRelocate(0, 0, glassPane.getWidth(), glassPane.getHeight());
}
}
There is a known issue on Charm 5.0 when you try hide a layer too soon.
When you show a layer, it takes some time to do the rendering layout, and even without an animation transition, there is a few milliseconds gap between the time you show the layer and time it is finally shown.
If you call Layer::hide
before the layer is shown, the call will bail out, and the layer won't be hidden.
An easy test is the following:
private long time;
BusyLayer busyLayer = new BusyLayer();
busyLayer.setOnShowing(e -> {
time = System.currentTimeMillis();
System.out.println("Showing busyLayer");
});
busyLayer.setOnShown(e -> {
System.out.println("busyLayer shown in: " + (System.currentTimeMillis() - time) + " ms");
});
busyLayer.setOnHiding(e -> System.out.println("hiding layer at " + (System.currentTimeMillis() - time) + " ms"));
busyLayer.show();
Now let's say you have a long task that takes one second:
PauseTransition p = new PauseTransition(Duration.seconds(1));
p.setOnFinished(f -> busyLayer.hide());
p.play();
then the layer will be hidden as expected.
But if the task is way faster and it takes a few milliseconds:
PauseTransition p = new PauseTransition(Duration.seconds(0.01));
p.setOnFinished(f -> busyLayer.hide());
p.play();
it is possible that the layer is not shown yet, and the hide()
call will fail.
Workaround
While this is fixed properly, a possible workaround is to listen to the Layer's LifecycleEvent.SHOWN
event, and do something like:
private BooleanProperty shown = new SimpleBooleanProperty();
BusyLayer busyLayer = new BusyLayer();
busyLayer.setOnShowing(e -> shown.set(false));
busyLayer.setOnShown(e -> shown.set(true));
busyLayer.show();
PauseTransition p = new PauseTransition(taskDuration);
p.setOnFinished(f -> {
if (shown.get()) {
// layer was shown, hide it
busyLayer.hide();
} else {
// layer is not shown yet, wait until it does, and hide
shown.addListener(new InvalidationListener() {
@Override
public void invalidated(Observable observable) {
if (shown.get()) {
busyLayer.hide();
shown.removeListener(this);
}
}
});
}
});
p.play();
Edit
I'm adding a possible BusyLayer
implementation:
class BusyLayer extends Layer {
private final GlassPane glassPane = MobileApplication.getInstance().getGlassPane();
private final StackPane root;
private final double size = 150;
public BusyLayer() {
root = new StackPane(new ProgressIndicator());
root.setStyle("-fx-background-color: white;");
getChildren().add(root);
setBackgroundFade(0.5);
}
@Override
public void layoutChildren() {
super.layoutChildren();
root.setVisible(isShowing());
if (!isShowing()) {
return;
}
root.resize(size, size);
resizeRelocate((glassPane.getWidth() - size)/2, (glassPane.getHeight()- size)/2, size, size);
}
}
EDIT
The main issue is related to how BusyLayer
overrides the Layer::layoutChildren
method.
As you can read read here for Layer::layoutChildren
:
Override this method to add the layout logic for your layer. Care should be taken to call this method in overriden methods for proper functioning of the Layer.
This means that you have to call super.layoutChildren()
to get the layer properly working.
@Override
public void layoutChildren() {
super.layoutChildren();
// add your own implementation
}
This is a usual pattern when JavaFX built-in controls are extended.