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

JavaFX中如何让方法调用等待动画完成 实现消息队列顺序发送

解决方案

每次调用sendMessage()都会新建独立Timeline实例,多个实例同时修改标签内容就会出现动画并行的异常。针对多入口调用的场景,用FIFO消息队列+单活动动画的方案即可实现顺序播放,不需要阻塞调用,完全适配JavaFX单UI线程模型。

核心改动点

  • 新增并发安全的消息队列存储所有待播放的通知文本,所有调用sendMessage()传入的消息先入队,不直接触发动画
  • 全局只维护一个活动的动画实例,同一时间最多只有一个打字动画在运行
  • 给动画加结束回调:当前文本的打字动画播放完成后,自动从队列头部取下一条消息继续播放,队列为空时停止动画等待新消息
  • 增加JavaFX应用线程校验,非UI线程调用方法也不会抛出线程安全异常

修改后的完整代码

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public class AnimationTest extends Application {

    private final Label messageLabel = new Label();
    // 并发安全的消息队列,存储所有待播放的通知
    private final Queue<String> messageQueue = new ConcurrentLinkedQueue<>();
    // 全局唯一的当前活动动画引用
    private Timeline currentTimeline;

    @Override
    public void start(Stage stage) {
        VBox vBox = new VBox(10);
        vBox.setAlignment(Pos.CENTER);
        Scene scene = new Scene(vBox, 320, 240);
        vBox.getChildren().add(messageLabel);
        Button button = new Button("发送通知");
        button.setOnAction(event -> sendMessage("Some animated text."));
        vBox.getChildren().add(button);
        stage.setScene(scene);
        stage.show();
    }

    private void sendMessage(String message) {
        // 非FX应用线程的调用自动转义到UI线程执行,避免线程安全问题
        if (!Platform.isFxApplicationThread()) {
            Platform.runLater(() -> sendMessage(message));
            return;
        }
        // 新消息入队
        messageQueue.offer(message);
        // 当前没有正在运行的动画,立即启动播放流程
        if (currentTimeline == null || currentTimeline.getStatus() != Animation.Status.RUNNING) {
            playNextMessage();
        }
    }

    private void playNextMessage() {
        String nextMessage = messageQueue.poll();
        // 队列已空,清空动画引用等待新消息
        if (nextMessage == null) {
            currentTimeline = null;
            return;
        }

        final IntegerProperty charIndex = new SimpleIntegerProperty(0);
        currentTimeline = new Timeline();
        KeyFrame keyFrame = new KeyFrame(
                Duration.millis(40),
                event -> {
                    if (charIndex.get() > nextMessage.length()) {
                        currentTimeline.stop();
                        // 当前消息播放完成,自动处理队列中下一条
                        playNextMessage();
                        return;
                    }
                    messageLabel.setText(nextMessage.substring(0, charIndex.get()));
                    charIndex.set(charIndex.get() + 1);
                });
        currentTimeline.getKeyFrames().add(keyFrame);
        currentTimeline.setCycleCount(Animation.INDEFINITE);
        currentTimeline.play();
    }

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

补充说明

  • 不管有多少个节点、多少个线程调用sendMessage(),所有消息都会严格按照调用顺序排队播放,不会出现动画重叠的问题
  • 实现没有引入任何阻塞逻辑,不会造成UI卡顿
  • 如果需要实现消息优先级、消息去重之类的需求,只需要替换队列实现即可,改动成本极低
  • 如果需要在两条消息之间加停留间隔,只需要在动画结束后加一个PauseTransition再调用playNextMessage()即可

内容的提问来源于stack exchange,提问作者Ernest P W

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 15:09:46