现在有一个Node,它的坐标是(x, y),我们想要把这个Node平移到(x1, y1)的位置,但是普通的线性动画不好看,于是就想到了在JavaFX中实现一个曲线变速动画。
现在有2个问题。1,用什么代表曲线;2,用什么工具实现。我们依次来解决这两个问题。
用函数图像来代表曲线
数学中的函数是一种对应关系
对于一个特定的 x ,一定有一个唯一的 y 与之对应。我们可以利用这个特性建立组件坐标与时间 t 的函数 ,让每一个特定的时间点,组件有一个确定的位置。数学中有非常多的函数图像,有二次函数,三次函数,正弦函数,余弦函数,我们可以利用这些函数图像,来描述组件的位置。
用Timeline动画实现动画帧
Timeline
是JavaFX中最基本的动画工具,它可以按照指定的时间间隔执行一系列动作。KeyFrame
是Timeline
的基本单位,包含了特定时间点上要执行的动作。
因此,我们可以利用Timeline,结合数学函数,来控制一个组件在某一时刻的位置。理论存在,实践开始。
确定函数图像
为了简化问题,这里给出一个假设。有一个时长总共为 5s 的曲线变速动画,该动画能够将一个矩形(Rectange)从(0,0)的位置移动到(500, 0)的位置。也就是在X方向上向右平移了500个单位。由于只平移了X坐标,因此我们在这里将问题简化为X坐标上的曲线变速平移分析。
我们决定使用正弦函数的图像,取[0, π]的区间,如果 v = sin x, 那么 x ∈[0, π] 的图像刚好满足一个组件曲线加速后再曲线减速。
我们知道,如果用正弦函数的图像做vt图像,取x∈[0, π]的话,那么组件移动过的距离等于vt图像所围的面积,而
我们想要让组件移动500单位的距离,而不是2个单位的距离,很显然,用正弦函数图像是不行的,要用正弦型函数图像。
由于速度总从0开始,t=0时v=0,于是设组件的速度函数
这里应该有当 t∈[0, 5000] 时,wt∈[0, π],于是解得
这样我们就能得到,每一毫秒组件的瞬时速率。而vt图像的面积代表位移,于是得到
整理得
利用一点点现代科技解出A
于是得到
假设时间为t时,这个组件的位置为,不难得出
那么,再次利用一丢丢的现代科技
就能够得到
很明显我们的 t = 5000(ms) 在 (0, 10000]范围内,该式成立。接下来进入代码实操
代码部分
我们创建一个 takeCurveShiftXTranslateAnimation 方法用于实现我们的功能。
public static void takeCurveShiftXTranslateAnimation(Node node,
IntToDoubleFunction compute,
int millis) {
Timeline timeline = new Timeline();
for (int i = 0; i < millis; i ++) {
int finalI = i;
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(i), event -> node.setLayoutX(compute.applyAsDouble(finalI))));
}
timeline.play();
}
这里,node是我们需要操作的组件,compute是一个函数式接口,用于接受一个int,也就是动画进行的时间,返回一个Double,也就是返回我们的,这里让调用者自己实现compute,可以实现更多函数的曲线变速效果。millis是动画时间。
方法做的事情很简单,为每一毫秒添加一个关键帧用于移动组件。每一帧做的操作就是将组件移动到指定的位置。
用类似的方法还可以写 takeCurveShiftXTranslateAnimation 用于操作Y坐标,这里不在阐述。
接下来让我们实现利用正弦函数的compute部分
public static void takeSinCSTranslateAnimation(Node node, double fromX, double toX, int millis) {
if (millis > 10000)
throw new IllegalArgumentException("'millis' CANNOT bigger than 10000!");
takeCurveShiftXTranslateAnimation(node, mil -> fromX + (250 - 250 * Math.cos((Math.PI/5000) * mil)), millis);
}
这里面的
mil -> fromX + (250 - 250 * Math.cos((Math.PI/5000) * mil))
其实就是实现了
这个公式。只不过,我们推导这个公式时假设的组件起始位置是0,而在代码中加入了fromX作为起始位置罢了。
注意到这段代码其实还有优化空间。由于Timeline动画需要非常多的KeyFrame,这意味着如果要实现一个相对长时间的动画就需要new非常多的对象。这里5000ms的动画就new了5000个KeyFrame。由于动画只进行setLayoutX操作,因此我们完全可以抛弃KeyFrame,采用一个double[]来保存每一毫秒node的LayoutX,然后遍历这个double[],每一毫秒执行一次Platform.runLater(...),里面加上node.setLayoutX的代码,最终实现相同的效果。
这里没有给出优化完的代码是因为,作者写完这篇博客去吃饭了,回来重新看才发现这个性能问题。因此这里不再给出代码,只是给出优化思路,当然也许还有更优的优化思路,具体代码还望各位读者自行编写。
另外再说一下。上面的代码只可借鉴不可copy。请不要直接cv到你的项目里。因为上面的代码并不是通用代码,实际上是为上面的假设“一个组件X方向偏移500单位用时5s”写的,具体原因就在于推导时,偷懒直接使用了时间5000ms和距离500单位,直接带入算式计算了。这就导致了上面的代码仅仅只适用于上面的假设。如果你想要实现其他的效果,请自行推导
公式,并按照自己的公式重写compute,否则你直接copy出来的效果将与本博客的效果大相径庭。
测试
JavaFX实现的曲线变速动画