Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Instant animation with KeyValue when using the overlay parent height as the target value

I previously posted a question about why the overlay animation in the StackPane container occurs instantly when using KeyFrame.

@Slaw pointed out the root of the issue in a comment:

You're using on-finished handlers of KeyFrame. Those are executed once at the end of the frame's duration. They do not cause the value to change incrementally over time.

And to solve it:

For that, pass a KeyValue when creating the KeyFrame

He also suggested reading his answer to a previously posted question, explaining how similar animations should be handled.

Solution

Following @slaw, answer: the OverLayAnimation class handles the overlay and iconLabel animations, respectively, using ParallelTransition, for both sliding the overlay down and fading the label in. playForward() playBackward() handles both events MOUSE_ENTERED MOUSE_EXITED

Problem

The createToBottomAnimation() returns a Timeline for transitioning the overlay. When the Keyframe's to value is hardcoded (200, for example), the animation occurs smoothly and the max height updates gradually. However, the issue is that when resizing the window, the overlay will not fill all its container since its parent has grown and the value is hardcoded.

For that, I set the value to overlay.getParent().getMaxHeight(). The overlay does fill its container now, but no animation is occurring

Code

import javafx.animation.*;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.Objects;

public class Example extends Application {
    private ImageView createImageIcon(String path) {
        Image image = new Image(Objects.requireNonNull(getClass().getResourceAsStream(path)));
        ImageView imageView = new ImageView(image);
        imageView.setPreserveRatio(true);
        imageView.setFitHeight(200);
        imageView.setFitWidth(200);

        return imageView;
    }

    private Pane crateImageIconContainer(ImageView image, VBox overlay, String iconTitle, HBox rootElement) {
        Pane region = new Pane();
        HBox.setMargin(region, new Insets(30, 30, 30, 30));
        region.maxWidthProperty().bind(rootElement.widthProperty().divide(3));
        region.maxHeightProperty().bind(region.widthProperty());
        region.setPrefSize(400, 400);

        StackPane stackableContainer = new StackPane();
        HBox.setMargin(stackableContainer, new Insets(30, 30, 30, 30));
        stackableContainer.prefWidthProperty().bind(region.widthProperty());
        stackableContainer.prefHeightProperty().bind(region.heightProperty());

        stackableContainer.setStyle("-fx-background-color: #ff0000;");

        region.getChildren().add(stackableContainer);

        image.fitWidthProperty().bind(stackableContainer.widthProperty().divide(1.5));
        image.fitHeightProperty().bind(stackableContainer.heightProperty().divide(1.5));

        Label iconLabel = new Label(iconTitle);
        iconLabel.setOpacity(0);
        overlay.getChildren().add(iconLabel);

        overlay.setAlignment(Pos.CENTER);
        StackPane.setAlignment(overlay, Pos.TOP_CENTER);
        overlay.setStyle("-fx-background-color: #ffffff;");
        overlay.setOpacity(0.8);
        overlay.setMaxHeight(0);

        stackableContainer.getChildren().addAll(image, overlay);

        var animation = new OverLayAnimation(overlay, iconLabel);
        stackableContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> {
            e.consume();
            animation.playForward();
        });
        stackableContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
            e.consume();
            animation.playBackward();
        });
        return region;
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        HBox mainContainer = new HBox();
        HBox.setHgrow(mainContainer, Priority.ALWAYS);
        mainContainer.setAlignment(Pos.CENTER);
        mainContainer.setStyle("-fx-background-color: #00396c;");

        ImageView photoIcon = createImageIcon("../resources/photo.png");
        ImageView videoIcon = createImageIcon("../resources/video.png");
        ImageView webCamIcon = createImageIcon("../resources/webcam.png");

        Pane photoIconContainer = crateImageIconContainer(photoIcon, new VBox(), "Photo", mainContainer);
        Pane videoIconContainer = crateImageIconContainer(videoIcon, new VBox(), "Video", mainContainer);
        Pane webCamIconContainer = crateImageIconContainer(webCamIcon, new VBox(), "Web Cam", mainContainer);

        mainContainer.getChildren().addAll(photoIconContainer, videoIconContainer, webCamIconContainer);

        Scene scene = new Scene(mainContainer, 900, 350);
        primaryStage.setTitle("Color isolator");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private static class OverLayAnimation {
        private final VBox overlay;
        private final Label iconLabel;
        private static final Duration DURATION = Duration.millis(350);
        private final ParallelTransition transition;

        OverLayAnimation(VBox overlay, Label iconLabel) {
            this.overlay = overlay;
            this.iconLabel = iconLabel;

            var toBottom = createToBottomAnimation();
            var fadeInOut = createFadeInAndOutAnimation();

            transition = new ParallelTransition(toBottom, fadeInOut);
        }

        void playForward() {
            transition.setRate(1);
            transition.play();
        }

        void playBackward() {
            // Does not execute
            // transition.setRate(-1);
            // transition.play();

            KeyValue x = new KeyValue(overlay.maxHeightProperty(), 0);
            KeyFrame frame = new KeyFrame(DURATION, x);
            Timeline timeline = new Timeline(frame);
            timeline.play();
        }

        private Animation createToBottomAnimation() {
            // Animate instantly
            KeyValue x = new KeyValue(overlay.maxHeightProperty(), ((Pane) overlay.getParent()).getMaxHeight());

            // Animate smoothly
            // KeyValue x = new KeyValue(overlay.maxHeightProperty(), 200);
            KeyFrame frame = new KeyFrame(DURATION, x);
            return new Timeline(frame);
        }

        private Animation createFadeInAndOutAnimation() {
            var fadeInOut = new FadeTransition(DURATION, iconLabel);
            fadeInOut.setFromValue(0);
            fadeInOut.setToValue(1);
            return fadeInOut;
        }
    }
}

Results

This is how the instant animation look like. And this is how the animation is supposed to look like, but since the to value is 200, it will not fill the container.

I would like to know why the animation occurs instantly and, if possible, why setRate(-1) in playBackward() does not cause the keyFrame to go backward with the overlay animation.

like image 301
Starnec Avatar asked Oct 29 '25 05:10

Starnec


1 Answers

I recommend against binding the sizes of nodes to sizes of their parents.

The JavaFX layout mechanism implicitly defines a dependency of the size of a parent node on the sizes of its child nodes. If you bind the size of a child node to the size of its parent, you create a circular dependency, from which strange things can happen.

To make a child node's size dependent on the parent node, either (the most common case) use a layout pane with constraints that support the dependency you want to implement, or create a custom region, and implement the layoutChildren() method to resize and position the child nodes depending on the size available. In the following example I use the first method (specifically, a GridPane with column constraints) to position the three panes equally across the width of the container, and the second technique to position an image in the center of the pane, sized to the maximum size available (while respecting the preserveRatio property, or at least acknowledging that the size of the image may not be the size requested by fitWidth and fitHeight), with an overlay placed over it.

The strategy I'd recommend for sliding an overlay across another node, and retracting it, is to define a property representing how much of the overlay is visible, as a proportion from 0 (no overlay) to 1 (overlay completely covering the node). In the layout, place the overlay directly on top of the node with the same position and same size. Add a clip to the overlay, and set the size of the clip based on the size of the node/overlay and the property (so, e.g. use a rectangle for the clip with the height of the rectangle equal to the height of the node, multiplied by the property value).

In this example I define a subclass of Region that positions the image and its overlay, and supports an overlayProportion property as described above. When the property changes, a layout pass is requested (via an invalidation listener). A rectangular clip is used, extending down from the top, but you can also experiment with other options (e.g. a circle or making the rectangle spread from the center, etc.).

Now all that the animation has to do is to animate the value of the overlayProportionProperty between 0 and 1. A simple Timeline can do this.

Here is a full solution; I modified the original code to generate an on-the-fly image so it can be run without access to any resources.

import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Example extends Application {
    private ImageView createImageIcon(Color baseColor) {
//        Image image = new Image(Objects.requireNonNull(getClass().getResourceAsStream(path)));
        WritableImage image = new WritableImage(2,2);
        PixelWriter pw = image.getPixelWriter();
        pw.setColor(0, 0, baseColor);
        pw.setColor(0, 1, baseColor.darker());
        pw.setColor(1, 0, baseColor.darker());
        pw.setColor(1, 1, baseColor);
        ImageView imageView = new ImageView(image);
        imageView.setPreserveRatio(true);
        imageView.setFitHeight(200);
        imageView.setFitWidth(200);

        return imageView;
    }

    private Region createImageIconContainer(ImageView image, String iconTitle) {
        Label label = new Label(iconTitle);
        VBox overlay = new VBox(label);
        overlay.setMouseTransparent(true);
        overlay.setOpacity(0.8);
        ImageOverlayContainer imageOverlayContainer = new ImageOverlayContainer(image, overlay);
        imageOverlayContainer.setPadding(new Insets(30));
        imageOverlayContainer.setPrefSize(400, 400);
        imageOverlayContainer.setStyle("-fx-background-color: #ff0000;");

        overlay.setAlignment(Pos.TOP_CENTER);
        overlay.setStyle("-fx-background-color: #ffffff;");
        overlay.setOpacity(0.8);

        Timeline timeline = new Timeline(
                new KeyFrame(Duration.ZERO, new KeyValue(imageOverlayContainer.overlayProportionProperty(), 0.0)),
                new KeyFrame(Duration.millis(350), new KeyValue(imageOverlayContainer.overlayProportionProperty(), 1.0))
        );

        image.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> {
            e.consume();
            timeline.setRate(1);
            timeline.play();
        });
        image.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
            e.consume();
            timeline.setRate(-1);
            timeline.play();
        });
        return imageOverlayContainer;
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane mainContainer = new GridPane();
        mainContainer.setAlignment(Pos.CENTER);
        mainContainer.setStyle("-fx-background-color: #00396c;");
        ColumnConstraints cc = new ColumnConstraints();
        cc.setPercentWidth(100.0/3);
        mainContainer.getColumnConstraints().addAll(cc, cc, cc);

        ImageView photoIcon = createImageIcon(Color.RED);
        ImageView videoIcon = createImageIcon(Color.GREEN);
        ImageView webCamIcon = createImageIcon(Color.BLUE);

        Region photoIconContainer = createImageIconContainer(photoIcon, "Photo");
        Region videoIconContainer = createImageIconContainer(videoIcon, "Video");
        Region webCamIconContainer = createImageIconContainer(webCamIcon, "Web Cam");

        mainContainer.addRow(0, photoIconContainer, videoIconContainer, webCamIconContainer);

        Scene scene = new Scene(mainContainer, 900, 350);
        primaryStage.setTitle("Color isolator");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private static class ImageOverlayContainer extends Region {
        private final ImageView imageView;
        private final Node overlay ;
        private final Rectangle overlayClip = new Rectangle(0, 0, 0, 0);

        private final DoubleProperty overlayProportion = new SimpleDoubleProperty();
        public DoubleProperty overlayProportionProperty() {
            return overlayProportion;
        }
        public final double getOverlayProportion() {
            return overlayProportionProperty().get();
        }
        public final void setOverlayProportion(double overlayProportion) {
            overlayProportionProperty().set(overlayProportion);
        }

        public ImageOverlayContainer(ImageView imageView, Node overlay) {
            this.imageView = imageView;
            this.overlay = overlay;
            this.overlay.setClip(overlayClip);
            getChildren().addAll(imageView, overlay);
            overlayProportion.addListener(_ -> requestLayout());
        }
        @Override
        protected void layoutChildren() {
            double availWidth = snapSizeX(getWidth() - getInsets().getLeft() - getInsets().getRight());
            double availHeight = snapSizeY(getHeight() - getInsets().getTop() - getInsets().getBottom());
            imageView.setFitWidth(availWidth);
            imageView.setFitHeight(availHeight);

            double imgW = imageView.getBoundsInLocal().getWidth();
            double imgH = imageView.getBoundsInLocal().getHeight();

            double x = snapPositionX(getInsets().getLeft() + (availWidth - imgW) / 2);
            double y = snapPositionY(getInsets().getTop() + (availHeight - imgH) / 2);

            imageView.setX(x);
            imageView.setY(y);

            overlayClip.setWidth(imgW);
            overlayClip.setHeight(imgH * Math.clamp(getOverlayProportion(), 0, 1));
            overlay.resizeRelocate(x, y, imgW, imgH);

        }
    }
}
like image 121
James_D Avatar answered Oct 31 '25 20:10

James_D



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!