javajavafxfxml

Specifying keyframe animation in FXML


If keyframe animation can be specified in FXML, then a lot of file formats containing animations can be ported to FXML without information loss. I'm aware of the simpler variants like RotateTransition.

The most straightforward approach of declaring the KeyValue target as $text.translateX (or the other variations I could think of gave type-checking runtime errors. After some fiddling, I managed to come up with code that doesn't give errors, but the target binding also doesn't seem to happen. I've added a line to the controller for that to allow easy testing of what the wanted behavior is. How can I get rid of that line and make it work just with FXML? Shouldn't it also work with $text.translateX?

animated.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.animation.*?>
<?import javafx.beans.property.SimpleDoubleProperty?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.Group?>
<?import javafx.scene.Scene?>
<?import javafx.scene.text.Text?>
<?import javafx.util.Duration?>
<?import java.lang.Double?>
<Scene xmlns:fx="http://javafx.com/fxml" fx:controller="org.example.demo.AnimatedController">
    <height>240.0</height>
    <width>320.0</width>
    <fx:define>
        <SimpleDoubleProperty fx:id="count"/>
        <Timeline fx:id="timeline">
            <keyFrames>
                <fx:define>
                    <Double fx:id="endValue0" fx:value="0.0"/>
                    <Double fx:id="endValue1" fx:value="100.0"/>
                </fx:define>
                <KeyFrame fx:id="keyFrame0">
                    <values>
                        <KeyValue fx:id="keyValue0" endValue="$endValue0">
                            <target>
                                <fx:reference source="count"/>
                            </target>
                        </KeyValue>
                    </values>
                    <time>
                        <Duration fx:constant="ZERO"/>
                    </time>
                </KeyFrame>
                <KeyFrame fx:id="keyFrame1">
                    <values>
                        <KeyValue fx:id="keyValue1" endValue="$endValue1">
                            <target>
                                <fx:reference source="count"/>
                            </target>
                        </KeyValue>
                    </values>
                    <time>
                        <Duration millis="1000"/>
                    </time>
                </KeyFrame>
            </keyFrames>
        </Timeline>
    </fx:define>
    <Group>
        <Text fx:id="text" y="60" x="${count.value}">Hello, World!</Text>
        <Button onAction="#play">Play</Button>
    </Group>
</Scene>

AnimatedController.java

package org.example.demo;

import javafx.animation.Timeline;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.text.Text;

public class AnimatedController {

    @FXML
    private Text text;

    @FXML
    private Timeline timeline;

    @FXML
    private SimpleDoubleProperty count = new SimpleDoubleProperty(0);

    public void play(ActionEvent ignoredActionEvent) {
        text.xProperty().bind(count); // without this line, text.getX() doesn't get updated
        count.addListener((_, oldVal, newVal) -> System.out.printf("%s, %s, %s\n", text.getX(), oldVal, newVal));
        timeline.play();
    }
}

HelloApplication.java

package org.example.demo;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("animated.fxml"));
        Scene scene = fxmlLoader.load();
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}

Solution

  • From some experimentation, the following worked:

    <KeyValue target="$someNode.translateXProperty" endValue="..."/>
    

    Why does that work? Not sure. I was unable to find anything in the Introduction to FXML document that explained this behavior, but maybe someone else knows if and where it is documented. That said, apparently using xxxProperty gives you the actual property associated with xxx instead of just the property's value.

    Note this solution makes the intermediate property unnecessary.


    Example

    Tested with JavaFX 22 (but not with other JavaFX versions).

    Main.java

    package com.example;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    
    public class Main extends Application {
    
      @Override
      public void start(javafx.stage.Stage primaryStage) throws Exception {
        var loader = new FXMLLoader();
        loader.setLocation(Main.class.getResource("/Main.fxml"));
    
        var root = loader.<Parent>load();
        var controller = loader.<Controller>getController();
    
        primaryStage.setScene(new Scene(root, 600, 400));
        primaryStage.show();
    
        controller.playAnimation();
      }
    }
    

    Controller.java

    package com.example;
    
    import javafx.animation.Timeline;
    import javafx.fxml.FXML;
    
    public class Controller {
    
      @FXML private Timeline timeline;
    
      public void playAnimation() {
        timeline.play();
      }
    }
    

    Main.fxml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import java.lang.Double?>
    <?import javafx.animation.Animation?>
    <?import javafx.animation.KeyFrame?>
    <?import javafx.animation.KeyValue?>
    <?import javafx.animation.Timeline?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.layout.StackPane?>
    <?import javafx.util.Duration?>
    
    <StackPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
        fx:controller="com.example.Controller">
    
        <Label fx:id="label" text="Hello, World!" />
    
        <fx:define>
            <Double fx:id="fromValue" fx:value="-100.0" />
            <Double fx:id="toValue" fx:value="100.0" />
    
            <Duration fx:id="startTime" fx:constant="ZERO" />
            <Duration fx:id="endTime" millis="1000.0" />
    
            <Timeline fx:id="timeline" autoReverse="true">
                <cycleCount>
                    <Animation fx:constant="INDEFINITE" />
                </cycleCount>
                <keyFrames>
                    <KeyFrame time="$startTime">
                        <values>
                            <KeyValue target="$label.translateXProperty" endValue="$fromValue" />
                        </values>
                    </KeyFrame>
                    <KeyFrame time="$endTime">
                        <values>
                            <KeyValue target="$label.translateXProperty" endValue="$toValue" />
                        </values>
                    </KeyFrame>
                </keyFrames>
            </Timeline>
        </fx:define>
    
    </StackPane>