ZetCode

JavaFX 动画

最后修改于 2023 年 10 月 18 日

在本章中,我们使用 JavaFX 中的动画。 我们使用 AnimationTimer, TransitionTimeline 创建动画。

动画 是图像的快速连续,产生运动的幻觉。 然而,动画并不局限于运动。 随着时间的推移改变节点的背景也被认为是一种动画。

JavaFX 提供了三个基本工具来创建动画

AnimationTimer 是创建动画的最简单的工具。 它是一个基本的计时器; 它的 handle 方法在动画的每一帧中被调用。 Transition 是一个定义动画的基本高级框架。 动画由 interpolate 方法的 frac 值控制。 Timeline 是用于进行高级动画的最复杂的工具。 Timeline 动画使用 KeyFrames 定义,它概述了节点在指定时间点的目标值,对于沿 Timeline 插值的变量集。 动画属性由 KeyValues 定义。

Animation 类

Animation 是 JavaFX 中定义高级动画的基本类。 TransitionTimeline 都扩展了 Animation。 动画通过 playplayFromStart 方法启动,并通过 stop 方法结束。 可以通过调用 pause 方法暂停动画,并且下一次 play 调用会从暂停的位置恢复动画。 rate 属性定义了动画预计播放的方向和速度。 delay 属性指定动画的初始延迟量。 动画可以循环运行; 循环次数在 cycleCount 属性中定义,cycleDuration 指示一个循环的持续时间。 可以使用 autoReverseProperty 在交替循环中反转动画。

JavaFX AnimationTimer

AnimationTimer 允许创建一个计时器,该计时器在它处于活动状态时在每一帧中被调用。 它是一个抽象类; 因此,我们需要创建一个扩展它的自定义类。 它的 handle 方法(在每一帧中被调用)必须被重写。 AnimationTimerstart 方法启动计时器,而 stop 方法停止它。

com/zetcode/AnimationTimerEx.java
package com.zetcode;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;

public class AnimationTimerEx extends Application {

    private double opacity = 1;
    private Label lbl;

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new StackPane();

        lbl = new Label("JavaFX");
        lbl.setFont(Font.font(48));
        root.getChildren().add(lbl);

        AnimationTimer timer = new MyTimer();
        timer.start();

        var scene = new Scene(root, 300, 250);

        stage.setTitle("AnimationTimer");
        stage.setScene(scene);
        stage.show();
    }

    private class MyTimer extends AnimationTimer {

        @Override
        public void handle(long now) {

            doHandle();
        }

        private void doHandle() {

            opacity -= 0.01;
            lbl.opacityProperty().set(opacity);

            if (opacity <= 0) {

                stop();
                System.out.println("Animation stopped");
            }
        }
    }

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

该示例使用 AnimationTimer 在节点上创建淡出效果。

lbl = new Label("JavaFX");
lbl.setFont(Font.font(48));
root.getChildren().add(lbl);

我们的动画更改此 Label 控件的属性。

AnimationTimer timer = new MyTimer();
timer.start();

创建一个 AnimationTimer 并调用其 start 方法。

private class MyTimer extends AnimationTimer {

    @Override
    public void handle(long now) {

        doHandle();
    }
...
}

我们创建了 AnimationTimer 的一个具体子类并重写了它的 handle 方法。

private void doHandle() {

    opacity -= 0.01;
    lbl.opacityProperty().set(opacity);

    if (opacity <= 0) {

        stop();
        System.out.println("Animation stopped");
    }
}

doHandle 方法中,我们减少了 opacity 变量并更新了 opacityProperty。 如果 opacity 达到其最小值,则计时器会使用其 stop 方法停止。

JavaFX FadeTransition

Transition 动画最适合计划好的动画。 Transition 具有可以用于创建各种动画的具体类,这些动画可以并行或顺序执行; 例如 FadeTransition, PathTransition, RotateTransitionScaleTransition

FadeTransition 创建一个淡入淡出效果的动画,该动画跨越其持续时间。 这是通过以规则的间隔更新节点的 opacity 变量来完成的。

com/zetcode/FadeTransitionEx.java
package com.zetcode;

import javafx.animation.Animation;
import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class FadeTransitionEx extends Application {

    private FadeTransition ft;
    private Rectangle rect;

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new Group();

        rect = new Rectangle(20, 20, 150, 150);
        rect.setOnMouseClicked(new RectClickHandler());

        ft = new FadeTransition(Duration.millis(5000), rect);
        ft.setFromValue(1.0);
        ft.setToValue(0.0);

        root.getChildren().add(rect);

        var scene = new Scene(root, 300, 250);

        stage.setTitle("Fading transition");
        stage.setScene(scene);
        stage.show();
    }

    private class RectClickHandler implements EventHandler<MouseEvent> {

        @Override
        public void handle(MouseEvent event) {

            doHandle();
        }

        private void doHandle() {

            double opa = rect.getOpacity();

            if ((int) opa == 0) {
                return;
            }

            Animation.Status as = ft.getStatus();

            if (as == Animation.Status.RUNNING) {
                return;
            }

            if (as == Animation.Status.STOPPED) {
                ft.play();
            }
        }
    }

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

此示例使用 FadeTransition 在一个矩形上创建一个淡出效果。 在矩形区域内单击鼠标后,动画开始。

rect = new Rectangle(20, 20, 150, 150);
rect.setOnMouseClicked(new RectClickHandler());

将鼠标单击处理程序设置为矩形。

ft = new FadeTransition(Duration.millis(5000), rect);

创建 FadeTransition。 它的第一个参数是过渡的持续时间。 第二个参数是要更新其 opacity 参数的节点。

ft.setFromValue(1.0);
ft.setToValue(0.0);

setFromValue 设置不透明度的起始值,而 setToValue 设置结束不透明度值。

double opa = rect.getOpacity();

当前的不透明度值由 getOpacity 方法确定。

if (opa.intValue() == 0) {
    return;
}

矩形淡出后,我们取消鼠标单击。

Animation.Status as = ft.getStatus();

if (as == Animation.Status.RUNNING) {
    return;
}

if (as == Animation.Status.STOPPED) {
    ft.play();
}

getStatus 方法确定过渡的状态。 如果状态是 Animation.Status.STOPPED,我们使用 play 方法启动过渡。

PathTransition

PathTransition 沿路径创建动画。 沿路径的平移是通过更新节点的 translateXtranslateY 变量来完成的。 请注意,我们必须使用支持元素绝对定位的节点。

com/zetcode/PathTransitionEx.java
package com.zetcode;

import javafx.animation.PathTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
import javafx.util.Duration;

public class PathTransitionEx extends Application {

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new Pane();

        var path = new Path();
        path.getElements().add(new MoveTo(20, 120));
        path.getElements().add(new CubicCurveTo(180, 60, 250, 340, 420, 240));

        var circle = new Circle(20, 120, 10);
        circle.setFill(Color.CADETBLUE);

        var ptr = new PathTransition();

        ptr.setDuration(Duration.seconds(6));
        ptr.setDelay(Duration.seconds(2));
        ptr.setPath(path);
        ptr.setNode(circle);
        ptr.setCycleCount(2);
        ptr.setAutoReverse(true);
        ptr.play();

        root.getChildren().addAll(path, circle);

        var scene = new Scene(root, 450, 300);

        stage.setTitle("PathTransition");
        stage.setScene(scene);
        stage.show();
    }

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

该示例使用 PathTransition 沿路径移动一个圆。 动画在初始延迟 2 秒后开始。 它由两个循环组成。 动画被反转; 也就是说,圆从起始点移动到结束点,然后返回。

var root = new Pane();

我们使用 Pane 作为我们的根节点。 它支持动画所需的绝对定位。

var path = new Path();
path.getElements().add(new MoveTo(20, 120));
path.getElements().add(new CubicCurveTo(180, 60, 250, 340, 420, 240));

在这里,我们定义了动画对象将沿其移动的 Path

var circle = new Circle(20, 120, 10);
circle.setFill(Color.CADETBLUE);

这个圆是我们的动画中的移动对象。

var ptr = new PathTransition();

创建一个 PathTransition 对象。

ptr.setDuration(Duration.seconds(6));

setDuration 方法设置动画的持续时间。

ptr.setDelay(Duration.seconds(2));

setDelay 方法设置动画的初始延迟。

ptr.setPath(path);
ptr.setNode(circle);

setPath 方法设置路径,setNode 设置动画的目标节点。

ptr.setCycleCount(2);

我们的动画有两个循环。 循环次数由 setCycleCount 方法设置。

ptr.setAutoReverse(true);

使用 setAutoReverse 方法,我们反转动画的方向。 圆圈向后移动到起始位置。

ptr.play();

最后,play 方法启动动画。

PathTransition
图:PathTransition

ParallelTransition

ParallelTransition 并行播放一系列 Animations

com/zetcode/ParallelTransitionEx.java
package com.zetcode;

import javafx.animation.FillTransition;
import javafx.animation.ParallelTransition;
import javafx.animation.RotateTransition;
import javafx.animation.ScaleTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;


public class ParallelTransitionEx extends Application {

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new Pane();

        var rect = new Rectangle(50, 50, 30, 30);
        rect.setArcHeight(10);
        rect.setArcWidth(10);
        rect.setFill(Color.CADETBLUE);

        var rottr = new RotateTransition(Duration.millis(2000), rect);
        rottr.setByAngle(180);
        rottr.setCycleCount(2);
        rottr.setAutoReverse(true);

        var sctr = new ScaleTransition(Duration.millis(2000), rect);
        sctr.setByX(2);
        sctr.setByY(2);
        sctr.setCycleCount(2);
        sctr.setAutoReverse(true);

        var fltr = new FillTransition(Duration.millis(2000), rect,
                Color.CADETBLUE, Color.STEELBLUE);
        fltr.setCycleCount(2);
        fltr.setAutoReverse(true);

        root.getChildren().add(rect);

        var ptr = new ParallelTransition();
        ptr.getChildren().addAll(rottr, sctr, fltr);

        ptr.play();

        Scene scene = new Scene(root, 300, 250);

        stage.setTitle("ParallelTransition");
        stage.setScene(scene);
        stage.show();
    }

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

该示例并行播放三个过渡。 有一个矩形对象,它被旋转、缩放,并且其背景颜色发生变化。

var rottr = new RotateTransition(Duration.millis(2000), rect);
rottr.setByAngle(180);
rottr.setCycleCount(2);
rottr.setAutoReverse(true);

RotateTransition 将矩形旋转指定的角度。 旋转发生在两个循环中并被反转。

var sctr = new ScaleTransition(Duration.millis(2000), rect);
sctr.setByX(2);
sctr.setByY(2);

ScaleTransition 将矩形放大和缩小 2 倍。

var fltr = new FillTransition(Duration.millis(2000), rect,
    Color.CADETBLUE, Color.STEELBLUE);

FillTransition 将矩形的填充颜色从一个颜色值更改为另一个颜色值。

var ptr = new ParallelTransition();
ptr.getChildren().addAll(rottr, sctr, fltr);

ptr.play();

三种类型的过渡被放置在 ParallelTransition 中,它并行播放它们,即同时播放它们。

SequentialTransition

SequentialTransition 以顺序播放一系列 Animations

com/zetcode/SequentialTransitionEx.java
package com.zetcode;

import javafx.animation.FillTransition;
import javafx.animation.RotateTransition;
import javafx.animation.ScaleTransition;
import javafx.animation.SequentialTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;


public class SequentialTransitionEx extends Application {

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new Pane();

        var rect = new Rectangle(50, 50, 30, 30);
        rect.setArcHeight(10);
        rect.setArcWidth(10);
        rect.setFill(Color.CADETBLUE);

        var rottr = new RotateTransition(Duration.millis(2000), rect);
        rottr.setByAngle(180);
        rottr.setCycleCount(2);
        rottr.setAutoReverse(true);

        var sctr = new ScaleTransition(Duration.millis(2000), rect);
        sctr.setByX(2);
        sctr.setByY(2);
        sctr.setCycleCount(2);
        sctr.setAutoReverse(true);

        var fltr = new FillTransition(Duration.millis(2000), rect,
                Color.CADETBLUE, Color.STEELBLUE);
        fltr.setCycleCount(2);
        fltr.setAutoReverse(true);

        root.getChildren().add(rect);

        var str = new SequentialTransition();
        str.getChildren().addAll(rottr, sctr, fltr);

        str.play();

        var scene = new Scene(root, 300, 250);

        stage.setTitle("SequentialTransition");
        stage.setScene(scene);
        stage.show();
    }

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

该示例按顺序播放三个过渡,一个接一个。

var str = new SequentialTransition();
str.getChildren().addAll(rottr, sctr, fltr);

str.play();

这三个过渡被添加到 SequentialTransition

Timeline

Timeline 是在 JavaFX 中创建动画的最复杂工具。 动画使用 KeyFrames 定义,其中包含更改的节点属性。 这些属性被封装在 KeyValues 中。 Timeline 插入属性的变化。

com/zetcode/TimelineEx.java
package com.zetcode;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.effect.Lighting;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;


public class TimelineEx extends Application {

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new Pane();

        var rect = new Rectangle(20, 20, 60, 60);
        rect.setEffect(new Lighting());
        rect.setFill(Color.CADETBLUE);

        var tl = new Timeline();

        tl.setCycleCount(2);
        tl.setAutoReverse(true);

        var kv = new KeyValue(rect.translateXProperty(), 200);
        var kf = new KeyFrame(Duration.millis(2000), kv);
        tl.getKeyFrames().addAll(kf);

        tl.play();

        root.getChildren().addAll(rect);

        var scene = new Scene(root, 350, 250);

        stage.setTitle("Timeline");
        stage.setScene(scene);
        stage.show();
    }

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

该示例使用 Timeline 为矩形设置动画。

var rect = new Rectangle(20, 20, 60, 60);
rect.setEffect(new Lighting());
rect.setFill(Color.CADETBLUE);

这个矩形是动画中的移动对象。

var tl = new Timeline();

创建一个 Timeline 对象。

tl.setCycleCount(2);
tl.setAutoReverse(true);

动画由两个循环组成,并被反转。 矩形前后移动。

var kv = new KeyValue(rect.translateXProperty(), 200);

KeyValue 包含随时间变为 200 的 translateX 属性。

var kf = new KeyFrame(Duration.millis(2000), kv);

实例化一个 KeyFrame。 第一个参数是它的持续时间,第二个是 KeyValue。 动画持续 2 秒,在此期间其 translateX 属性更改为 200。

tl.getKeyFrames().addAll(kf);

关键帧被添加到帧列表中。

顺序 Timeline 动画

我们没有在时间轴中定义所有关键帧。 我们定义了一些帧,其余的被插值。 关键帧提供指定时间点的目标值,用于在时间轴中插值的一组变量。 为了按顺序执行关键帧,我们使用 SequentialTransition 类。

com/zetcode/SequentialTimelineEx.java
package com.zetcode;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.SequentialTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class SequentialTimelineEx extends Application {

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new Pane();

        var c = new Circle(50, 100, 10);
        c.setFill(Color.CADETBLUE);

        var kv1 = new KeyValue(c.scaleXProperty(), 4);
        var kv2 = new KeyValue(c.scaleYProperty(), 4);
        var kf1 = new KeyFrame(Duration.millis(3000), kv1, kv2);

        var scale = new Timeline();
        scale.getKeyFrames().add(kf1);

        var kv3 = new KeyValue(c.centerXProperty(), 250);
        var kf2 = new KeyFrame(Duration.millis(5000), kv3);

        var move = new Timeline();
        move.getKeyFrames().add(kf2);

        var kv4 = new KeyValue(c.scaleXProperty(), 1);
        var kv5 = new KeyValue(c.scaleYProperty(), 1);
        var kf3 = new KeyFrame(Duration.millis(3000), kv4, kv5);

        var scale2 = new Timeline();
        scale2.getKeyFrames().add(kf3);

        var seqtr = new SequentialTransition(scale, move, scale2);
        seqtr.play();

        root.getChildren().add(c);

        var scene = new Scene(root, 300, 250);

        stage.setTitle("Sequential Timeline animation");
        stage.setScene(scene);
        stage.show();
    }

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

该示例创建了一个顺序时间轴动画。 动画由三个 Timelines 组成,这些 Timelines 使用 SequentialTransition 顺序执行。

var c = new Circle(50, 100, 10);
c.setFill(Color.CADETBLUE);

此动画中的移动对象是一个 Circle

var kv1 = new KeyValue(c.scaleXProperty(), 4);
var kv2 = new KeyValue(c.scaleYProperty(), 4);
var kf1 = new KeyFrame(Duration.millis(3000), kv1, kv2);

var scale = new Timeline();
scale.getKeyFrames().add(kf1);

这是第一个 Timeline。 它在三秒钟内将圆放大。

var kv3 = new KeyValue(c.centerXProperty(), 250);
var kf2 = new KeyFrame(Duration.millis(5000), kv3);

var move = new Timeline();
move.getKeyFrames().add(kf2);

第二个 Timeline 向前移动圆。 动画的这部分持续五秒钟。

var kv4 = new KeyValue(c.scaleXProperty(), 1);
var kv5 = new KeyValue(c.scaleYProperty(), 1);
var kf3 = new KeyFrame(Duration.millis(3000), kv4, kv5);

var scale2 = new Timeline();
scale2.getKeyFrames().add(kf3);

第三个 Timeline 将圆缩小。

var seqtr = new SequentialTransition(scale, move, scale2);
seqtr.play();

这三个时间轴被放置在 SequentialTransition 中。 时间轴被顺序播放,一个接一个。

在本章中,我们介绍了 JavaFX 动画。