layergluonglasspane

Gluon mobile 5 layer does not hide


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

}


Solution

  • 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.