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 KeyFrame
s. Each KeyFrame
- 必须在
Timeline
(你传入的Duration
对象)上指定一个时间点
- 可以可选地指定一个
KeyValue
s的集合,这
包含 WritableValue
s(例如,Property
s)和目标
在那个时间点 WritableValue
的值
- 可以可选地指定要执行的操作,形式为
EventHandler<ActionEvent>
Timeline
有一个 currentTime
属性,随着 Timeline
正在播放,它(当然)会随着时间的流逝向前推进。 pause()
将停止 currentTime
的进展,使其固定在当前值。 stop()
将停止 currentTime
的进程并将 currentTime
设置回零。
如果 Timeline
具有指定 KeyValue
的 KeyFrame
,则随着 currentTime
的更改,WritableValue
中指定的 WritableValue
24=]s 将设置为取决于 currentTime
的值。 (具体来说,如果 WritableValue
是可插值的,则该值将在两个相邻的 KeyFrames
之间插值,为 WritableValue
指定 KeyValue
。否则,该值将被设置为"most recent" KeyFrame
指定了 WritableValue
的值。)
如果 Timeline
有 KeyFrame
指定操作 (EventHandler<ActionEvent>
s),那么随着 currentTime
的进展超过 [=20= 指定的时间],动作被调用。
为什么您的代码不适用于 stop()
或 playFromStart()
在您的例子中,您的 KeyFrame
指定了一个操作,它将新的转换添加到节点的转换列表中。请注意,这根本不依赖于 currentTime
,除了每次 currentTime
达到 0.04 seconds
时,都会添加一个新的转换(另外,任何实现方法 shiftAndScale
你没有显示)。因此,如果您 stop()
时间线,currentTime
将重置为零,但节点不会因此发生任何事情。 (实际上,currentTime
只在 0
和 0.04
秒之间变化。)
您的代码存在其他问题
您的代码存在问题,存在内存泄漏。 Node
维护 ObservableList
的 Transform
。您正在添加到此列表(非常频繁),但从未删除任何内容。 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
请注意,通过使用此技术,节点只会应用一个变换;我们只是随着我们的进步更新那个转换。舍入误差仍在累积,但至少代数是可行的:)。
我正在为文本编写时间轴:
用法:
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 KeyFrame
s. Each KeyFrame
- 必须在
Timeline
(你传入的Duration
对象)上指定一个时间点 - 可以可选地指定一个
KeyValue
s的集合,这 包含WritableValue
s(例如,Property
s)和目标 在那个时间点WritableValue
的值 - 可以可选地指定要执行的操作,形式为
EventHandler<ActionEvent>
Timeline
有一个 currentTime
属性,随着 Timeline
正在播放,它(当然)会随着时间的流逝向前推进。 pause()
将停止 currentTime
的进展,使其固定在当前值。 stop()
将停止 currentTime
的进程并将 currentTime
设置回零。
如果 Timeline
具有指定 KeyValue
的 KeyFrame
,则随着 currentTime
的更改,WritableValue
中指定的 WritableValue
24=]s 将设置为取决于 currentTime
的值。 (具体来说,如果 WritableValue
是可插值的,则该值将在两个相邻的 KeyFrames
之间插值,为 WritableValue
指定 KeyValue
。否则,该值将被设置为"most recent" KeyFrame
指定了 WritableValue
的值。)
如果 Timeline
有 KeyFrame
指定操作 (EventHandler<ActionEvent>
s),那么随着 currentTime
的进展超过 [=20= 指定的时间],动作被调用。
为什么您的代码不适用于 stop()
或 playFromStart()
在您的例子中,您的 KeyFrame
指定了一个操作,它将新的转换添加到节点的转换列表中。请注意,这根本不依赖于 currentTime
,除了每次 currentTime
达到 0.04 seconds
时,都会添加一个新的转换(另外,任何实现方法 shiftAndScale
你没有显示)。因此,如果您 stop()
时间线,currentTime
将重置为零,但节点不会因此发生任何事情。 (实际上,currentTime
只在 0
和 0.04
秒之间变化。)
您的代码存在其他问题
您的代码存在问题,存在内存泄漏。 Node
维护 ObservableList
的 Transform
。您正在添加到此列表(非常频繁),但从未删除任何内容。 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
请注意,通过使用此技术,节点只会应用一个变换;我们只是随着我们的进步更新那个转换。舍入误差仍在累积,但至少代数是可行的:)。