You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何降低Java中drawFrame()调用次数以实现动画慢动作?

如何降低JavaFX中drawFrame()的调用次数实现慢动作动画?

我正在学习David Eck所著的《Java入门》一书,书中GUI图形章节的嵌套矩形动画示例里,drawFrame()子例程每秒会被自动调用约60次。我希望降低该方法的调用次数,从而让矩形以慢动作移动。以下是完整代码:

package com.example.ch3movingrects;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.layout.BorderPane;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

/**
 *  When run as a program, this class opens a window on the screen that
 *  shows a set of nested rectangles that seems to be moving infinitely
 *  inward towards the center.  The animation continues until the user
 *  closes the window.
 */
public class ch3MovingRects extends Application {

    /**
     * Draws a set of nested rectangles. This subroutine is called 60 times per
     * second and is responsible for redrawing the entire drawing area.  The
     * parameter g is used for drawing. The frameNumber starts at zero and
     * increases by 1 each time this subroutine is called.  The parameters width
     * and height give the size of the drawing area, in pixels.
     * The sizes and positions of the rectangles that are drawn depend
     * on the frame number, giving the illusion of motion.
     */
    public void drawFrame(GraphicsContext g, int frameNumber, double elapsedSeconds, int width, int height) {

        g.setFill(Color.WHITE);
        g.fillRect(0,0,width,height);  // Fill drawing area with white.

        double inset; // Gap between edges of drawing area and the outer rectangle.

        double rectWidth, rectHeight;   // The size of one of the rectangles.

        g.setStroke(Color.BLACK);  // Draw the rectangle outlines in black.

        inset = frameNumber % 15 + 0.5; // The "+ 0.5" is a technicality to produce a sharper image.

        rectWidth = width - 2*inset;
        rectHeight = height - 2*inset;

        while (rectWidth >= 0 && rectHeight >= 0) {
            g.strokeRect(inset, inset, rectWidth, rectHeight);
            inset += 15;       // rectangles are 15 pixels apart
            rectWidth -= 30;
            rectHeight -= 30;
        }

    }

    //------ Implementation details: DO NOT EXPECT TO UNDERSTAND THIS ------

    public void start(Stage stage) {
        int width = 800;   // The width of the image.  You can modify this value!
        int height = 600;  // The height of the image. You can modify this value!
        Canvas canvas = new Canvas(width,height);
        drawFrame(canvas.getGraphicsContext2D(), 0, 0, width, height);
        BorderPane root = new BorderPane(canvas);
        root.setStyle("-fx-border-width: 4px; -fx-border-color: #444");
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setTitle("Infinte Moving Rects"); // STRING APPEARS IN WINDOW TITLEBAR!
        stage.show();
        stage.setResizable(false);
        AnimationTimer anim = new AnimationTimer() {
            private int frameNum;
            private long startTime = -1;
            private long previousTime;
            public void handle(long now) {
                if (startTime < 0) {
                    startTime = previousTime = now;
                    drawFrame(canvas.getGraphicsContext2D(), 0, 0, width, height);
                }
                else if (now - previousTime > 0.95e9/60) {
                    // The test in the else-if is to make sure that drawFrame() is
                    // called about once every 1/60 second.  It is required since
                    // handle() can be called by the system more often than that.
                    frameNum++;
                    drawFrame(canvas.getGraphicsContext2D(), frameNum, (now-startTime)/1e9, width, height);
                    previousTime = now;
                }
            }
        };
        anim.start();
    }

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

} // end MovingRects

解决方案

1. 修改AnimationTimer的调用间隔阈值

原代码中AnimationTimer的handle方法通过now - previousTime > 0.95e9/60判断来控制每秒约60次调用。要降低调用次数,直接修改分母数值即可:

  • 若要每秒30次调用,改为0.95e9/30
  • 若要每秒20次调用,改为0.95e9/20

修改后的核心代码片段:

else if (now - previousTime > 0.95e9/30) { // 调整为每秒30帧
    frameNum++;
    drawFrame(canvas.getGraphicsContext2D(), frameNum, (now-startTime)/1e9, width, height);
    previousTime = now;
}

2. 使用Timeline替代AnimationTimer(更直观控制帧率)

Timeline可以直接指定关键帧间隔,无需手动计算时间差,适合固定帧率场景。替换原AnimationTimer部分的代码:

// 替换原AnimationTimer代码块
import javafx.animation.Timeline;
import javafx.animation.KeyFrame;
import javafx.animation.Animation;
import javafx.util.Duration;

// 在start方法内:
int frameNum = 0;
Timeline timeline = new Timeline();
timeline.setCycleCount(Animation.INDEFINITE);
// 设置间隔:1000ms/30 ≈33.3ms,即每秒30帧
KeyFrame keyFrame = new KeyFrame(Duration.millis(1000/30), event -> {
    frameNum++;
    drawFrame(canvas.getGraphicsContext2D(), frameNum, frameNum/30.0, width, height);
});
timeline.getKeyFrames().add(keyFrame);
timeline.play();

3. 保持调用次数但减慢动画进度(可选)

如果不想降低drawFrame()调用次数,仅需动画变慢,可以修改frameNum的增长速率,比如每两次调用才让frameNum加1:

else if (now - previousTime > 0.95e9/60) {
    // 每两次调用才推进一次动画帧
    if (frameNum % 2 == 0) {
        frameNum++;
    }
    drawFrame(canvas.getGraphicsContext2D(), frameNum, (now-startTime)/1e9, width, height);
    previousTime = now;
}

这种方法本质上没有减少drawFrame()调用次数,仅通过减慢进度实现慢动作。

内容的提问来源于stack exchange,提问作者Tushar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 01:01:18