4

I've created my own Marquee class / control that works just fine after the first few seconds it's loaded. However, once the app first loads the marquee thinks that it's localbounds are maxWidth: 0.0 minWidth: 2.0. Below is the Marquee class code and the code I'm using to load it in my test app.

Marquee 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;
        }

    }

and my Main 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);
    }
}

I have tried waiting to load the marquee until after showing the stage and trying to get parentBounds, localBounds, you name it. It just always wants to start off at 0.0, 2.0.

Any advice would be greatly appreciated.

JeramyRR
  • 4,273
  • 3
  • 20
  • 20

2 Answers2

2

In your Marquee class, to animate the nodes you call getContentWidth(), which just gets the width of each node:

private int getContentsWidth(){
    int width = 0;
    for(Node node : getChildrenUnmodifiable()){
         width += node.boundsInLocalProperty().get().getWidth();
    }
    return width;
}

Then you instantiate your marquees, and start the animation:

Marquee marqueeLeft = new Marquee(labels, Duration.ZERO, Direction.LEFT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
marqueeLeft.animate();

And then you show the stage.

The problem with your approach is just in getWidth(): it will return always 0 until you show the stage and layout the nodes. After that, they will have a non-zero width.

Now, you have to wait for a first animation cycle. When the animation ends it calls animate() again:

public void recycleAnimation(){
    setDelay(Duration.ZERO);
    animate();
}

This second time, all the nodes are on the stage, and they have a valid width, and the marquee starts moving.

Solution: Just move the animation call:

Scene scene = new Scene(root,600,300);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();

marqueeLeft.animate();
marqueeRight.animate();

And now, in the sample, the width will be of 126 px, and the marquees will start moving as you expect.

EDIT

In case the marquees are created after the stage is shown:

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

This won't work either, since the call to animate() happens inmediately after the nodes are added to the scene, and the JavaFX Application Thread doesn't have time to update the values. You can read about the JavaFX architecture here.

Solution: let the scene graph some time to do its tasks:

root.setTop(marqueeLeft);
root.setBottom(marqueeRight);

Platform.runLater(new Runnable() {

    @Override
    public void run() {
        marqueeLeft.animate();
        marqueeRight.animate();
    }
});
José Pereda
  • 44,311
  • 7
  • 104
  • 132
  • Thank you! I guess when I tried to load it after showing the stage I was doing all the instantiation there as well. – JeramyRR Jan 15 '15 at 20:01
  • I've edited my answer with a suitable explanation of why that didn't work and a valid solution. – José Pereda Jan 15 '15 at 20:20
  • Thanks again! This was a real help. Now if I could just figure out how to keep my labels from overrunning... – JeramyRR Jan 15 '15 at 20:27
  • To avoid overrun, the label should never be smaller than it's preferred size. This will help: `label.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);` – José Pereda Jan 15 '15 at 20:42
2

You let JavaFX implicitly handle the layout pass during it's standard pulse process as suggested in Jose Pereda's answer OR you (if you use Java 8+) can 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.
Community
  • 1
  • 1
jewelsea
  • 150,031
  • 14
  • 366
  • 406