如何让 JavaFX 控件在初始化后立即实现它的 localBounds?
How do I get a JavaFX control to realize it's localBounds immediately upon initialization?
我已经创建了自己的 Marquee class / 控件,它在加载前几秒后工作正常。但是,一旦应用程序首次加载,选取框就会认为它的本地边界是 maxWidth: 0.0 minWidth: 2.0。下面是 Marquee class 代码和我用来在我的测试应用程序中加载它的代码。
选取框 Class:
public class Marquee extends HBox {
/**
* Add a node to the Marquee control
* @param node - a node to add
*/
public void add(Node node) {
getChildren().add(node);
}
/**
* Add a list of nodes to the Marquee control
* @param observable list - an observable list to add
*/
public void addAll(ObservableList<Node> list) {
getChildren().addAll(list);
}
/**
* Default Constructor: Initializes the Marquee Object with default settings:
* Empty Array List of Nodes, initial delay, Direction.LEFT, Duration.seconds(10), Interpolator.LINEAR, 10)
*/
public Marquee()
{
this(FXCollections.observableArrayList(new ArrayList<Node>()), Duration.seconds(3), Direction.LEFT, Duration.seconds(10), Interpolator.LINEAR, 10.0);
}
/**
* Constructor: Initializes the Marquee Object with default settings
* @param observable list
*/
public Marquee(ObservableList<Node> nodes)
{
this(nodes, Duration.seconds(3), Direction.LEFT, Duration.seconds(10), Interpolator.LINEAR, 10.0);
}
/**
* Constructor: Initializes the Marquee Object with default settings
* @param observable list
* @param duration - usually in seconds i.e. Duration.seconds(10)
*/
public Marquee(ObservableList<Node> nodes, Duration duration) {
this(nodes, Duration.seconds(3), Direction.LEFT, duration, Interpolator.LINEAR, 10.0);
}
/**
* Constructor: Initializes the Marquee Object with default settings
* @param observable list
* @param direction - an enum, i.e Direction.LEFT or Direction.RIGHT
* @param duration - usually in seconds i.e. Duration.seconds(10)
*/
public Marquee(ObservableList<Node> nodes, Direction direction, Duration duration) {
this(nodes, Duration.seconds(3), direction, duration, Interpolator.LINEAR, 10.0);
}
/**
* Constructor: Initializes the Marquee Object with default settings
* @param observable list
* @param duration - usually in seconds i.e. Duration.seconds(10)
* @param interpolator - effects the translation behavior, i.e
* Interpolator.EASE_BOTH, or EASE_LINEAR
*/
public Marquee(ObservableList<Node> nodes, Duration duration, Interpolator interpolator)
{
this(nodes, Duration.seconds(3), Direction.LEFT, duration, interpolator, 10.0);
}
/**
* Constructor: Initializes the Marquee Object with default settings:
* @param observable list
* @param initialDelay - the amount of time before the marquee will begin scrolling
* after the application has loaded
* @param direction - an enum, i.e Direction.LEFT or Direction.RIGHT
* @param duration - usually in seconds i.e. Duration.seconds(10)
* @param interpolator - effects the translation behavior, i.e
* Interpolator.EASE_BOTH, or EASE_LINEAR
*/
public Marquee(ObservableList<Node> list, Duration initialDelay, Direction direction, Duration duration, Interpolator interpolator) {
this(list, initialDelay, direction, duration, interpolator, 10.0);
}
/**
* Preferred Constructor: Initializes the Marquee Object with your preferred settings
*
* @param observable list
* @param initialDelay - the amount of time before the marquee will begin scrolling
* after the application has loaded
* @param direction - an enum, i.e Direction.LEFT or Direction.RIGHT
* @param duration - usually in seconds i.e. Duration.seconds(10)
* @param interpolator - effects the translation behavior, i.e
* Interpolator.EASE_BOTH, or EASE_LINEAR
* @param nodeSpacing - a double value that determines how far apart
* each element in the marquee will be placed from one another
*/
public Marquee(ObservableList<Node> list, Duration initialDelay, Direction direction, Duration duration, Interpolator interpolator, double nodeSpacing) {
super();
getChildren().addAll(list);
setSpacing(nodeSpacing);
delay = initialDelay;
this.direction = direction;
this.duration = duration;
this.interpolator = interpolator;
}
public enum Direction {
LEFT, RIGHT
};
private Direction direction;
private TranslateTransition animation;
private Duration duration;
/**
* This begins the animation of the Marquee. By default this method
* calculates the width of the Marquee's parent and uses that as its
* start point. When the nodes inside the Marquee have reached the outer
* bounds of its parent the Marquee will stop and reset. Note: If the
* application is resized, the animation will need to be stopped and
* restarted. The Marquee will recalculate its translation requirements each
* cycle so if the user resizes it's parent, the Marquee will conform.
*
* @param duration
* the amount of time the translation should take
*/
public void animate() {
animation = new TranslateTransition(duration, this);
double maxWidth = getBoundsInLocal().getMaxX();
double minWidth = getBoundsInLocal().getMinX() - getContentsWidth();
switch (direction) {
case LEFT:
animation.setToX(minWidth);
animation.setFromX(maxWidth);
break;
case RIGHT:
animation.setToX(maxWidth);
animation.setFromX(minWidth);
break;
default:
animation.setToX(minWidth);
animation.setFromX(maxWidth);
break;
}
animation.setCycleCount(1);
animation.setInterpolator(getInterpolator());
animation.setDelay(delay);
animation.playFromStart();
animation.setOnFinished(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event)
{
stopAnimation();
recycleAnimation();
}
});
}
private Duration delay;
public Duration getDelay() {
return delay;
}
public void setDelay(Duration delay) {
this.delay = delay;
}
private Interpolator interpolator;
/**
* How the Marquee transitions its content into and out of FOV.
* Options are:
* DISCRETE (DO NOT USE), EASE_IN, EASE_OUT, EASE_BOTH, and LINEAR (DEFAULT).
* Any change to the Interpolator will take affect after the current cycle
* ends.
* Suggested Usage: setInterpolator(Interpolator.LINEAR)
*/
public void setInterpolator(Interpolator interpolator)
{
this.interpolator = interpolator;
}
/**
* The Interpolator of the Marquee.
* @return Interpolator
*/
public Interpolator getInterpolator()
{
return interpolator;
}
public void recycleAnimation()
{
setDelay(Duration.ZERO);
animate();
}
/**
* Stop animation of Marquee
*/
public void stopAnimation() {
animation.stop();
}
/**
* Set the default spacing between nodes in the Marquee Default is set to
* 5.0
*/
public void setNodeSpacing(double value) {
setSpacing(value);
}
/**
* Get the current spacing between nodes in the Marquee
*
* @return double
*/
public double getNodeSpacing() {
return getSpacing();
}
private int getContentsWidth()
{
int width = 0;
for(Node node : getChildrenUnmodifiable())
{
width += node.boundsInLocalProperty().get().getWidth();
}
return width;
}
}
和我的主要 Class
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
ObservableList<Node> labels = FXCollections.observableArrayList();
labels.add(new Label("Test Label 1"));
labels.add(new Label("Test Label 2"));
Marquee marqueeLeft = new Marquee(labels, Duration.ZERO, Direction.LEFT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
root.setTop(marqueeLeft);
final ObservableList<Node> labels2 = FXCollections.observableArrayList();
labels2.add(new Label("Test Label 3"));
labels2.add(new Label("Test Label 4"));
final Marquee marqueeRight = new Marquee(labels2, Duration.ZERO, Direction.RIGHT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
root.setBottom(marqueeRight);
marqueeLeft.animate();
marqueeRight.animate();
Button button = new Button();
button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Workin");
marqueeRight.add(new Label("Test Add Label"));
}
});
root.setCenter(button);
Scene scene = new Scene(root,600,300);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
我试过等到显示舞台并尝试获取 parentBounds、localBounds 后加载选取框。它总是希望从 0.0、2.0 开始。
如有任何建议,我们将不胜感激。
在您的 Marquee
class 中,为您调用 getContentWidth()
的节点设置动画,它只获取每个节点的宽度:
private int getContentsWidth(){
int width = 0;
for(Node node : getChildrenUnmodifiable()){
width += node.boundsInLocalProperty().get().getWidth();
}
return width;
}
然后实例化选取框,并开始动画:
Marquee marqueeLeft = new Marquee(labels, Duration.ZERO, Direction.LEFT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
marqueeLeft.animate();
然后然后你展示舞台。
您的方法的问题就在于 getWidth()
:它将 return 始终为 0 直到 您显示舞台并布置节点。之后,它们将具有非零宽度。
现在,您必须等待第一个动画周期。当动画结束时,它会再次调用 animate()
:
public void recycleAnimation(){
setDelay(Duration.ZERO);
animate();
}
第二次,所有节点都在舞台上,并且有一个有效的宽度,跑马灯开始移动。
解决方法:移动动画调用即可:
Scene scene = new Scene(root,600,300);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
marqueeLeft.animate();
marqueeRight.animate();
现在,在示例中,宽度将为 126 像素,选取框将按照您的预期开始移动。
编辑
如果在创建字幕之后显示舞台:
Scene scene = new Scene(root,600,300);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
Marquee marqueeLeft = new Marquee(labels, Duration.ZERO, Direction.LEFT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
root.setTop(marqueeLeft);
marqueeLeft.animate();
这也行不通,因为对 animate()
的调用会在节点添加到场景后 立即 发生,而 JavaFX 应用程序线程不会有时间更新值。您可以阅读有关 JavaFX 架构的信息 here.
解决方案:让场景图有时间完成它的任务:
root.setTop(marqueeLeft);
root.setBottom(marqueeRight);
Platform.runLater(new Runnable() {
@Override
public void run() {
marqueeLeft.animate();
marqueeRight.animate();
}
});
你让 JavaFX 在其标准脉冲过程中隐式处理布局传递,如 Jose Pereda 的回答中所建议的,或者你(如果你使用 Java 8+)可以 manually applyCSS trigger a layout pass:
Pane parentNode = new Pane();
Scene scene = new Scene(parentNode);
Node child = new Button("XYZZY");
parentNode.getChildren().add(child);
System.out.println(button.getWidth()); // outputs 0 as width is not yet known.
parentNode.applyCss();
parentNode.layout();
System.out.println(button.getWidth()); // outputs non-0 as width is now known.
我已经创建了自己的 Marquee class / 控件,它在加载前几秒后工作正常。但是,一旦应用程序首次加载,选取框就会认为它的本地边界是 maxWidth: 0.0 minWidth: 2.0。下面是 Marquee class 代码和我用来在我的测试应用程序中加载它的代码。
选取框 Class:
public class Marquee extends HBox {
/**
* Add a node to the Marquee control
* @param node - a node to add
*/
public void add(Node node) {
getChildren().add(node);
}
/**
* Add a list of nodes to the Marquee control
* @param observable list - an observable list to add
*/
public void addAll(ObservableList<Node> list) {
getChildren().addAll(list);
}
/**
* Default Constructor: Initializes the Marquee Object with default settings:
* Empty Array List of Nodes, initial delay, Direction.LEFT, Duration.seconds(10), Interpolator.LINEAR, 10)
*/
public Marquee()
{
this(FXCollections.observableArrayList(new ArrayList<Node>()), Duration.seconds(3), Direction.LEFT, Duration.seconds(10), Interpolator.LINEAR, 10.0);
}
/**
* Constructor: Initializes the Marquee Object with default settings
* @param observable list
*/
public Marquee(ObservableList<Node> nodes)
{
this(nodes, Duration.seconds(3), Direction.LEFT, Duration.seconds(10), Interpolator.LINEAR, 10.0);
}
/**
* Constructor: Initializes the Marquee Object with default settings
* @param observable list
* @param duration - usually in seconds i.e. Duration.seconds(10)
*/
public Marquee(ObservableList<Node> nodes, Duration duration) {
this(nodes, Duration.seconds(3), Direction.LEFT, duration, Interpolator.LINEAR, 10.0);
}
/**
* Constructor: Initializes the Marquee Object with default settings
* @param observable list
* @param direction - an enum, i.e Direction.LEFT or Direction.RIGHT
* @param duration - usually in seconds i.e. Duration.seconds(10)
*/
public Marquee(ObservableList<Node> nodes, Direction direction, Duration duration) {
this(nodes, Duration.seconds(3), direction, duration, Interpolator.LINEAR, 10.0);
}
/**
* Constructor: Initializes the Marquee Object with default settings
* @param observable list
* @param duration - usually in seconds i.e. Duration.seconds(10)
* @param interpolator - effects the translation behavior, i.e
* Interpolator.EASE_BOTH, or EASE_LINEAR
*/
public Marquee(ObservableList<Node> nodes, Duration duration, Interpolator interpolator)
{
this(nodes, Duration.seconds(3), Direction.LEFT, duration, interpolator, 10.0);
}
/**
* Constructor: Initializes the Marquee Object with default settings:
* @param observable list
* @param initialDelay - the amount of time before the marquee will begin scrolling
* after the application has loaded
* @param direction - an enum, i.e Direction.LEFT or Direction.RIGHT
* @param duration - usually in seconds i.e. Duration.seconds(10)
* @param interpolator - effects the translation behavior, i.e
* Interpolator.EASE_BOTH, or EASE_LINEAR
*/
public Marquee(ObservableList<Node> list, Duration initialDelay, Direction direction, Duration duration, Interpolator interpolator) {
this(list, initialDelay, direction, duration, interpolator, 10.0);
}
/**
* Preferred Constructor: Initializes the Marquee Object with your preferred settings
*
* @param observable list
* @param initialDelay - the amount of time before the marquee will begin scrolling
* after the application has loaded
* @param direction - an enum, i.e Direction.LEFT or Direction.RIGHT
* @param duration - usually in seconds i.e. Duration.seconds(10)
* @param interpolator - effects the translation behavior, i.e
* Interpolator.EASE_BOTH, or EASE_LINEAR
* @param nodeSpacing - a double value that determines how far apart
* each element in the marquee will be placed from one another
*/
public Marquee(ObservableList<Node> list, Duration initialDelay, Direction direction, Duration duration, Interpolator interpolator, double nodeSpacing) {
super();
getChildren().addAll(list);
setSpacing(nodeSpacing);
delay = initialDelay;
this.direction = direction;
this.duration = duration;
this.interpolator = interpolator;
}
public enum Direction {
LEFT, RIGHT
};
private Direction direction;
private TranslateTransition animation;
private Duration duration;
/**
* This begins the animation of the Marquee. By default this method
* calculates the width of the Marquee's parent and uses that as its
* start point. When the nodes inside the Marquee have reached the outer
* bounds of its parent the Marquee will stop and reset. Note: If the
* application is resized, the animation will need to be stopped and
* restarted. The Marquee will recalculate its translation requirements each
* cycle so if the user resizes it's parent, the Marquee will conform.
*
* @param duration
* the amount of time the translation should take
*/
public void animate() {
animation = new TranslateTransition(duration, this);
double maxWidth = getBoundsInLocal().getMaxX();
double minWidth = getBoundsInLocal().getMinX() - getContentsWidth();
switch (direction) {
case LEFT:
animation.setToX(minWidth);
animation.setFromX(maxWidth);
break;
case RIGHT:
animation.setToX(maxWidth);
animation.setFromX(minWidth);
break;
default:
animation.setToX(minWidth);
animation.setFromX(maxWidth);
break;
}
animation.setCycleCount(1);
animation.setInterpolator(getInterpolator());
animation.setDelay(delay);
animation.playFromStart();
animation.setOnFinished(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event)
{
stopAnimation();
recycleAnimation();
}
});
}
private Duration delay;
public Duration getDelay() {
return delay;
}
public void setDelay(Duration delay) {
this.delay = delay;
}
private Interpolator interpolator;
/**
* How the Marquee transitions its content into and out of FOV.
* Options are:
* DISCRETE (DO NOT USE), EASE_IN, EASE_OUT, EASE_BOTH, and LINEAR (DEFAULT).
* Any change to the Interpolator will take affect after the current cycle
* ends.
* Suggested Usage: setInterpolator(Interpolator.LINEAR)
*/
public void setInterpolator(Interpolator interpolator)
{
this.interpolator = interpolator;
}
/**
* The Interpolator of the Marquee.
* @return Interpolator
*/
public Interpolator getInterpolator()
{
return interpolator;
}
public void recycleAnimation()
{
setDelay(Duration.ZERO);
animate();
}
/**
* Stop animation of Marquee
*/
public void stopAnimation() {
animation.stop();
}
/**
* Set the default spacing between nodes in the Marquee Default is set to
* 5.0
*/
public void setNodeSpacing(double value) {
setSpacing(value);
}
/**
* Get the current spacing between nodes in the Marquee
*
* @return double
*/
public double getNodeSpacing() {
return getSpacing();
}
private int getContentsWidth()
{
int width = 0;
for(Node node : getChildrenUnmodifiable())
{
width += node.boundsInLocalProperty().get().getWidth();
}
return width;
}
}
和我的主要 Class
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
ObservableList<Node> labels = FXCollections.observableArrayList();
labels.add(new Label("Test Label 1"));
labels.add(new Label("Test Label 2"));
Marquee marqueeLeft = new Marquee(labels, Duration.ZERO, Direction.LEFT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
root.setTop(marqueeLeft);
final ObservableList<Node> labels2 = FXCollections.observableArrayList();
labels2.add(new Label("Test Label 3"));
labels2.add(new Label("Test Label 4"));
final Marquee marqueeRight = new Marquee(labels2, Duration.ZERO, Direction.RIGHT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
root.setBottom(marqueeRight);
marqueeLeft.animate();
marqueeRight.animate();
Button button = new Button();
button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Workin");
marqueeRight.add(new Label("Test Add Label"));
}
});
root.setCenter(button);
Scene scene = new Scene(root,600,300);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
我试过等到显示舞台并尝试获取 parentBounds、localBounds 后加载选取框。它总是希望从 0.0、2.0 开始。
如有任何建议,我们将不胜感激。
在您的 Marquee
class 中,为您调用 getContentWidth()
的节点设置动画,它只获取每个节点的宽度:
private int getContentsWidth(){
int width = 0;
for(Node node : getChildrenUnmodifiable()){
width += node.boundsInLocalProperty().get().getWidth();
}
return width;
}
然后实例化选取框,并开始动画:
Marquee marqueeLeft = new Marquee(labels, Duration.ZERO, Direction.LEFT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
marqueeLeft.animate();
然后然后你展示舞台。
您的方法的问题就在于 getWidth()
:它将 return 始终为 0 直到 您显示舞台并布置节点。之后,它们将具有非零宽度。
现在,您必须等待第一个动画周期。当动画结束时,它会再次调用 animate()
:
public void recycleAnimation(){
setDelay(Duration.ZERO);
animate();
}
第二次,所有节点都在舞台上,并且有一个有效的宽度,跑马灯开始移动。
解决方法:移动动画调用即可:
Scene scene = new Scene(root,600,300);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
marqueeLeft.animate();
marqueeRight.animate();
现在,在示例中,宽度将为 126 像素,选取框将按照您的预期开始移动。
编辑
如果在创建字幕之后显示舞台:
Scene scene = new Scene(root,600,300);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
Marquee marqueeLeft = new Marquee(labels, Duration.ZERO, Direction.LEFT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
root.setTop(marqueeLeft);
marqueeLeft.animate();
这也行不通,因为对 animate()
的调用会在节点添加到场景后 立即 发生,而 JavaFX 应用程序线程不会有时间更新值。您可以阅读有关 JavaFX 架构的信息 here.
解决方案:让场景图有时间完成它的任务:
root.setTop(marqueeLeft);
root.setBottom(marqueeRight);
Platform.runLater(new Runnable() {
@Override
public void run() {
marqueeLeft.animate();
marqueeRight.animate();
}
});
你让 JavaFX 在其标准脉冲过程中隐式处理布局传递,如 Jose Pereda 的回答中所建议的,或者你(如果你使用 Java 8+)可以 manually applyCSS trigger a layout pass:
Pane parentNode = new Pane();
Scene scene = new Scene(parentNode);
Node child = new Button("XYZZY");
parentNode.getChildren().add(child);
System.out.println(button.getWidth()); // outputs 0 as width is not yet known.
parentNode.applyCss();
parentNode.layout();
System.out.println(button.getWidth()); // outputs non-0 as width is now known.