TLDR; I think layoutChildren should mention it is not a good practice to modify the list of children during the layout pass as CSS won't be applied to them for 1 rendered frame, and perhaps mention a solution (Scene#addPreLayoutPulseListener?)

I've been recently bit by an issue that I think might need to be documented a bit better.

LayoutChildren is typically overriden for custom layouts.

It's documentation:

     * Invoked during the layout pass to layout the children in this
     * {@code Parent}. By default it will only set the size of managed,
     * resizable content to their preferred sizes and does not do any node
     * positioning.
     * <p>
     * Subclasses should override this function to layout content as needed.

A custom layout can be simple, but sometimes may involve adding or removing children dynamically based on the content of the control. What layoutChildren here does not mention is that if you add children during layoutChildren, there will be 1 rendered frame where these children don't have any CSS styling applied. The system detects however that children were modified and layoutChildren will be called again (in the same pass AFAIK), resulting hopefully in a stable set of children.

The problem here is the fact that a frame is rendered without any CSS applied. For applications using a black background in general, the new children with a white background will cause a temporary "white flash" to be rendered. It took me ages to figure out where it came from, and it wasn't always easy to reproduce (if the children already existed, they were just modified, it only happened for cases where new children had to be added during the layout pass and which were clearly visible somewhere).

Obviously, changes to CSS files had no effect on solving this (like setting everything to have a black or transparent) background at ".root" level.

Calling #applyCSS on the newly added children during #layoutChildren does not solve the problem. Still one frame is rendered without CSS, and a white flash occurs.

Adding the children during the #computeXXX methods is too late as well, the CSS pass has finished already by that time.  This does however avoid having #layoutChildren called again in the same pass if children are added. There is this issue that relates to this: https://bugs.openjdk.org/browse/JDK-8091873 where it is suggested to perhaps add a more general method for this.

What I needed is a callback for my control before layout occurs and before CSS is applied.

Scene has a #addPreLayoutPulseListener which mentions it is called before CSS is applied. Its #addPostLayoutPulseListener counterpart also mentions `AnimationTimer` as a way to get a callback before CSS is applied. Implementing this call back and changing the list of children there solved the "white flash" problem. It feels a bit overkill though to listen to every pulse on a Scene just for the layout of one control (that may not need layout during most passes).

Aside from changing the documentation to ensure people understand that modifying the list of children during layout can have temporary CSS issues, I'm wondering what would be the preferred solution to this problem. Is there a different way of handling this that I haven't found, aside from the #addPreLayoutPulseListener or AnimationTimer ?  If not, could the issue (https://bugs.openjdk.org/browse/JDK-8091873) perhaps be resolved by adding two methods?  One that is called before CSS is applied, and one before any compute methods are called?  If adding a method that is called before CSS, is there even a way to determine if a Node needs this call to prevent calling it on every Node on every layout pulse?

The current solution I'm using is like 15 lines of code as part of a ListView skin:

    private final Runnable pulseListener = () -> content.manageCells();

    private void updatePreLayoutPulseListener(Scene old, Scene current) {
      if(old != null) {
        old.removePreLayoutPulseListener(pulseListener);
      }
      if(current != null) {
        current.addPreLayoutPulseListener(pulseListener);
      }
    }

And in the constructor:

    updatePreLayoutPulseListener(null, skinnable.getScene());

    skinnable.sceneProperty().addListener((obs, old, current) -> updatePreLayoutPulseListener(old, current));

And in dispose:

    updatePreLayoutPulseListener(skinnable.getScene(), null);

--John





Reply via email to