JavaFx。如何恢复?

JavaFx. How to resume?

我正在为文本编写时间轴:

用法:

Text text = new Text();
......
group.getChildren().addAll(text);
    root.getChildren().addAll(group);

tl.play();

这很好用。如果我想暂停并继续动画,tl.pause();tl.play(); 可以做到。

现在,我想让动画从头开始重新开始,我使用了tl.stop();tl.playFromStart();但是这个组合的效果和tl.pause();的效果是一样的& tl.play();

我的问题是,为什么 tl.playFromStart();无法正常工作以及如何恢复动画?

Timeline 的工作原理

一个Timeline represents a period of time over which an animation is performed. The Timeline comprises of a collection of KeyFrames. Each KeyFrame

  • 必须Timeline(你传入的Duration对象)上指定一个时间点
  • 可以可选地指定一个KeyValues的集合,这 包含 WritableValues(例如,Propertys)和目标 在那个时间点 WritableValue 的值
  • 可以可选地指定要执行的操作,形式为 EventHandler<ActionEvent>

Timeline 有一个 currentTime 属性,随着 Timeline 正在播放,它(当然)会随着时间的流逝向前推进。 pause() 将停止 currentTime 的进展,使其固定在当前值。 stop() 将停止 currentTime 的进程并将 currentTime 设置回零。

如果 Timeline 具有指定 KeyValueKeyFrame,则随着 currentTime 的更改,WritableValue 中指定的 WritableValue 24=]s 将设置为取决于 currentTime 的值。 (具体来说,如果 WritableValue 是可插值的,则该值将在两个相邻的 KeyFrames 之间插值,为 WritableValue 指定 KeyValue。否则,该值将被设置为"most recent" KeyFrame 指定了 WritableValue 的值。)

如果 TimelineKeyFrame 指定操作 (EventHandler<ActionEvent>s),那么随着 currentTime 的进展超过 [=20= 指定的时间],动作被调用。

为什么您的代码不适用于 stop()playFromStart()

在您的例子中,您的 KeyFrame 指定了一个操作,它将新的转换添加到节点的转换列表中。请注意,这根本不依赖于 currentTime,除了每次 currentTime 达到 0.04 seconds 时,都会添加一个新的转换(另外,任何实现方法 shiftAndScale你没有显示)。因此,如果您 stop() 时间线,currentTime 将重置为零,但节点不会因此发生任何事情。 (实际上,currentTime 只在 00.04 秒之间变化。)

您的代码存在其他问题

您的代码存在问题,存在内存泄漏。 Node 维护 ObservableListTransform。您正在添加到此列表(非常频繁),但从未删除任何内容。 Node 非常智能:它保留了一个隐藏矩阵,它是所有变换的净效果;当您添加一个新的转换时,它会将其存储在列表中,然后使用简单的矩阵乘法更新 "net" 矩阵。因此,您不会在这里看到任何计算性能问题:从这个角度来看,它可以很好地扩展。然而,它确实存储了所有单独的转换(因为,例如,它支持稍后删除它们),所以如果你让这个 运行 足够长,你将 最终 运行 内存不足。

您的代码的另一个(可能是次要的)问题是,当您组合所有这些转换时,您正在做大量的浮点运算。任何舍入误差最终都会累积。您应该尝试找到一种避免舍入误差累积的技术。

修复代码的方法

要解决此问题,您有两种选择:

如果动画是 "naturally cyclical"(意思是它 returns 在某个固定时间后进入其起始状态,比如旋转),那么只需根据该自然条件定义 Timeline期间。仅以旋转为例,您可以这样做:

double secondsPerCompleteCycle = (360.0 / 0.75) * 0.04 ;
Rotate rotation = new Rotate(0, new Point3D(1, 0, 0));
group.getTransforms().add(rotation);
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(secondsPerCompleteCycle), 
    new KeyValue(rotation.angleProperty(), 360, Interpolator.LINEAR)));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();

现在 timeline.stop() 会将 currentTime 设置为零,这将具有将旋转角度设置回其初始值(也为零)的效果。

如果动画不是自然重复的,我会使用(整数类型)计数器以您选择的任何时间单位跟踪 "current frame",然后将变换的值绑定到计数器。使用相同的例子,你可以做

double degreesPerFrame = 0.75 ;
LongProperty frameCount = new SimpleLongProperty();
Rotate rotation = new Rotate(0, new Point3D(1, 0, 0));
group.getTransforms().add(rotation);
rotation.angleProperty().bind(frameCount.multiply(degreesPerFrame));

Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(0.04), e -> 
    frameCount.set(frameCount.get() + 1)));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();

// to reset to the beginning:
timeline.stop();
frameCount.set(0L);

您也可以考虑使用 AnimationTimer,具体取决于您的具体要求。不过,我会先尝试其中一种技术。

在你的情况下,代数变得相当复杂(无论如何对我来说复杂得令人望而却步)。每个动作向节点添加三个转换;平移、缩放和绕 x 轴的旋转。这些的 4x4 矩阵表示是:

1 0 0 tx
0 1 0 ty
0 0 1 0
0 0 0 1

翻译,

sx  0 0 0 
 0 sy 0 0
 0  0 1 0
 0  0 0 1

为规模,

1      0       0 0
0 cos(t) -sin(t) 0
0 sin(t)  cos(t) 0
0      0       0 1

轮换。

虽然计算这三者的净效应并不难(只需将它们相乘即可),但我无法计算通过应用任意次数获得的净矩阵(也许...)。此外,您在 x 方向上平移的量正在发生变化,这几乎是不可能的。

因此,另一种解决方法是定义单个变换并将其应用于节点,然后在每个事件上对其进行修改。这看起来像

Affine transform = new Affine() ; // creates identity transform
node.getTransforms().add(transform);

Timeline timeline = new Timeline(Duration.seconds(0.04), event -> {
    double shiftX = ... ;
    double shiftY = ... ;
    double scaleX = ... ;
    double scaleY = ... ;
    double angle = 0.75 ;
    Affine change = new Affine();
    change.append(new Translate(shiftX, shiftY));
    change.append(new Scale(scaleX, scaleY));
    change.append(new Rotate(angle, new Point3D(1, 0, 0)));
    transform.append(change);
});

timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();

如上所述,stop()pause() 将具有(几乎)相同的效果。 (唯一的区别是当你再次玩时到第一次新更新的时间,对于 stop() 它将是 0.04 秒,对于 pause() 它会更少 - 直到下一次更新暂停时剩余的时间.) 但是对于 "reset" 动画,你只需要

timeline.stop();
transform.setToIdentity(); // resets to beginning

请注意,通过使用此技术,节点只会应用一个变换;我们只是随着我们的进步更新那个转换。舍入误差仍在累积,但至少代数是可行的:)。