LayoutChildren documentation
John Hendrikx
john.hendrikx at gmail.com
Sun Sep 11 06:08:36 UTC 2022
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
More information about the openjfx-dev
mailing list