1

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());
}

}

tbeernot
  • 2,473
  • 4
  • 24
  • 31

1 Answers1

1

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.

José Pereda
  • 44,311
  • 7
  • 104
  • 132
  • I'm afraid that is not the problem, the easy test code says: hiding layer at 2242 ms. That should be long enough, I figure. – tbeernot Nov 01 '18 at 16:15
  • The time base depends on what other activities are being done in the JavaFX application thread (switching views, for instance). I've done the test from the `postInit` method while switching to the home view and it takes around 0.6 seconds, while the same test in a given view takes 0.03 seconds. If you wait til the layer is shown, it should be hidden. Else, there is an issue with how you create the layer. – José Pereda Nov 01 '18 at 16:21
  • Just in case, I've added my `BusyLayer` implementation – José Pereda Nov 01 '18 at 16:25
  • I'll take a look. What I see is that the "busyLayer shown in ..." event is never fired, but the layer is visible. – tbeernot Nov 01 '18 at 16:33
  • You mean that you if show the layer, and don't call hide, it will never print `busyLayer shown`? – José Pereda Nov 01 '18 at 16:35
  • It never prints "busyLayer shown", no matter if I call hide or not. The sequence is as follows: call show, start a CompletableFuture, in its thenAcceptAsync use Platform.runLater to call hide. That seems to be correct? Your BusyLayer implementation resembles mine a lot. – tbeernot Nov 01 '18 at 19:12
  • Hmm, the fact that the `SHOWN` event is not triggered is not a good signal. Somehow you might be blocking the JavaFX thread? If you can post some more code to reproduce the issue, that would help. – José Pereda Nov 01 '18 at 19:30
  • You could get a copy of the sources. It's my JavaOne 2017 talk, but in real life used, Gluon app. You could then easily run and debug it. We would need to post the conclusion back here. It's not complex and has a demo mode. – tbeernot Nov 01 '18 at 21:35
  • If I show a Toast instead of a Layer it works fine (except it is not blocking out any UI interaction ofcourse). – tbeernot Nov 03 '18 at 06:28
  • I'm testing on desktop, adding three dummy tasks, and it works fine with the Layer. If all the tasks end too soon, you'll want to add the listener I mention in my answer. – José Pereda Nov 03 '18 at 11:57
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/183050/discussion-between-tbeernot-and-jose-pereda). – tbeernot Nov 03 '18 at 12:43
  • No, I didn't find the link – José Pereda Nov 07 '18 at 09:02
  • It relies on localMaven, and there are missing dependencies, can't login to nexus.softworks.nl... – José Pereda Nov 07 '18 at 11:05
  • Removed the local dependencies. The softworks repo proxies a number of other repo's, removed those as well. Can you give it another try? – tbeernot Nov 07 '18 at 11:49
  • @tbeernot You didn’t test my answer did you? And if you did, did you notice something _slightly_ different between my layer’s `layoutChildren` method and yours? Double check again!! Hint: `super.layoutChildren()` ... – José Pereda Nov 11 '18 at 16:57
  • Yes, I did; copying in your implementation was simple, so of course. I noticed that the sizing didn't work as I wanted, so I reverted back. I did even notice that one line difference. But I see that it works now with that line add. That is weird. – tbeernot Nov 12 '18 at 05:28
  • I don't see how that is weird. You have to call `super` to get all the stuff done by the `Layer::layoutChildren`. For instance, the events `SHOWN` and `HIDDEN` are fired there. – José Pereda Nov 12 '18 at 08:56
  • No. I mean weird that when I used your example it did not work. Anyhow, thanks for the effort! – tbeernot Nov 12 '18 at 10:15
  • Ok, it worked for me on your project. Btw, you might want to remove the link from the comments. – José Pereda Nov 12 '18 at 10:39