当前位置:首页 » 《资源分享》 » 正文

桌面宠物开发——罗小黑(一)_白宝库

15 人参与  2022年01月08日 10:01  分类 : 《资源分享》  评论

点击全文阅读


文章目录

  • 写在前面
  • 第一版雏形
  • 项目目录
  • 主入口程序
  • 全局常量
  • 动作
    • 动作基本类
    • 动作生成者
    • 动作执行者
  • 事件
  • 资源加载器
  • 交互平台


写在前面

我的QQ宠物已经读大学啦,每天吃得饱饱的,每次回到家,我学习的时候也会让她也学习,我想着以后大学我一定有更多的时间陪她,可谁知2018年9月15日她却回到了自己的故乡。
在这里插入图片描述
时间过得也快,转眼就大三了,这些年也学了不少知识,爱上了动漫《罗小黑战记》,谁知又要停更三年呢?罗小黑说:“我想和小白一起学读书!”,好呀~那就来读书吧!

想过多种语言来编写,比如C#、Python、C++,但还是选择了自己最熟悉的Java,谢谢燕然都护的博客给的思路,打算做一个更完整的桌面宠物,模仿原来QQ宠物的饥饿度、健康值、心情值、金币系统、学习成长系统,结合番剧、电影的故事背景添加法力值等等等等。最终做成一款可安装式的C/S架构的游戏,让喜欢罗小黑战记的人在三年的等待时间内都可以来陪伴他~

第一版雏形

请添加图片描述

项目目录

使用的IDE是JetBrains Intellij IDEA,新建一个普通的Java项目,这个为项目目录
在这里插入图片描述

主入口程序

整个程序从主入口程序进入,下面是HelloHeiApplication类源码:

package org.taibai.hellohei;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.taibai.hellohei.constant.Constant;
import org.taibai.hellohei.event.GlobalEventListener;
import org.taibai.hellohei.img.ResourceGetter;
import org.taibai.hellohei.ui.InterfaceFunction;

import java.io.IOException;


public class HelloHeiApplication extends Application {

    /**
     * 展示图片的窗口
     */
    private ImageView imageView;
    private AnchorPane pane;
    private InterfaceFunction interfaceFunction;
    /**
     * 全局事件监听,目前支持拖拽、左键点击反馈
     */
    private GlobalEventListener globalEventListener;

    private final ResourceGetter resourceGetter = ResourceGetter.newInstance();

    @Override
    public void start(Stage primaryStage) throws IOException {
        primaryStage.initStyle(StageStyle.UTILITY);
        primaryStage.setOpacity(0);     // 设置父级透明度为0
        Stage stage = new Stage();
        stage.initOwner(primaryStage);  // 将 primaryStage 设置为归属对象,即父级窗口
        initImageView();
        // 交互功能平台
        interfaceFunction = new InterfaceFunction(stage, imageView);
        // 面板
        pane = new AnchorPane(interfaceFunction.getMessageBox(), interfaceFunction.getImageView());
        pane.setStyle("-fx-background:transparent;");
        // 开启全局事件
        globalEventListener = new GlobalEventListener(stage, imageView, pane);
        initStage(stage);
        primaryStage.show();
        stage.show();
        interfaceFunction.setTray(stage);   //添加系统托盘
    }

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

    private void initImageView() {
        Image image = resourceGetter.get(Constant.ImageShow.mainImage);
        this.imageView = new ImageView(image);
        imageView.setX(0);
        imageView.setY(0);
        imageView.setLayoutX(0);
        imageView.setLayoutY(50);
        imageView.setFitHeight(Constant.ImageShow.ImageHeight); // 设置图片显示的大小
        imageView.setFitHeight(Constant.ImageShow.ImageWidth);
        imageView.setPreserveRatio(true);                       // 保留width:height比例
        imageView.setStyle("-fx-background:transparent;");      // 透明背景
    }

    private void initStage(Stage stage) {
        Scene scene = new Scene(pane, 400, 400);
        scene.setFill(null);
        scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
        stage.setScene(scene);
        // 设置窗体的初始位置
        stage.setX(850);
        stage.setY(400);
        stage.setAlwaysOnTop(true);// 窗口总显示在最前
        // 修改任务栏图标
        stage.getIcons().add(resourceGetter.get(Constant.ImageShow.iconImage));
        stage.initStyle(StageStyle.TRANSPARENT);// 背景透明
        stage.setOnCloseRequest(event -> {
            event.consume();
            interfaceFunction.exit();
        });
    }

}

依次说明一下:

  1. primaryStage并非正在的stage,在start方法中又新建了一个Stage实例,并且让primaryStage隐藏,这样做的目的是就不会在任务栏里面显示进程了,否则强迫症患者会很难受的。
  2. ImageView将作为整个程序展示的窗口,一系列动作也只是替换gif图片罢了
  3. 交互平台InterfaceFunction提供了用户的一系列交互动作,例如显示隐藏、退出、切换状态,以后的各种功能也将在交互平台扩展
  4. 全局事件监听者GlobalEventListener:考虑到各事件之间可能会相互干扰,于是开了一个类去集中管理。意在解决重复触发点击事件、拖动时不触发点击事件等问题。这样也让start方法更轻便一些。
  5. 最后将交互平台加入系统托盘,于是你可以在任务栏里像找到QQ程序一样找到小黑后台

全局常量

虽然全局常量不太好,目前功能单一,全局常量有助于调试,希望在后面能选择更好的解决方案

package org.taibai.hellohei.constant;

/**
 * <p>Creation Time: 2021-09-21 18:00:49</p>
 * <p>Description: 各种常量,集中管理</p>
 *
 * @author 太白
 */
public class Constant {

    public static class ImageShow {
        /**
         * 主体的长与宽
         */
        public static final int ImageHeight = 100;
        public static final int ImageWidth = 100;

        public static final String mainImage = "/org/taibai/hellohei/img/licking the claw.gif";
        public static final String byeImage = "/org/taibai/hellohei/img/bye.gif";
        public static final String iconImage = "/org/taibai/hellohei/img/icon.png";
        public static final String guitarImage = "/org/taibai/hellohei/img/playing guitar.gif";
    }

    public static class UserInterface {
        /**
         * 交互时间,例如点击罗小黑会响应一个动作,该动作持续RunTime
         */
        public static final int RunTime = 3;

        /**
         * 碎碎念
         */
        public static final String[] selfTalking = {
                "嘿咻~",
                "点我~",
                "小白,这个字怎么念呀",
                "想吃甘蔗了……",
                "在干嘛呢~"
        };
    }

}

动作

动作基本类

一个动作应该有如下属性

  • path: 该动作是什么
  • time: 执行多少时间
  • isTemporaryAction: 是否是暂时的,比如点击后触发的动作是展示显示的,而恢复到默认状态是持续的
  • recoverPath: 如果是暂时的那么应该恢复到什么动作
  • interruptable: 是否可中断的,例如退出动画是不可中断的,而在做普通动画是可中断的,这样退出动画就得以显示
package org.taibai.hellohei.ui;

/**
 * <p>Creation Time: 2021-09-22 11:49:27</p>
 * <p>Description: 动作</p>
 *
 * @author 太白
 */
public class Action {

    /**
     * 当前动作
     */
    private final String path;

    /**
     * 动作维持时间,如果为-1则保持该动作
     */
    private final double time;

    /**
     * 是否是暂时的动作
     */
    private final boolean isTemporaryAction;

    /**
     * 如果是暂时的动作,则应当在该时间内恢复到这个动作
     */
    private String recoverPath;

    /**
     * 是否可中断
     */
    private final boolean interruptable;

    /**
     * 若动作是持续的,则维持时间为 PerpetualTime
     */
    public static final double PerpetualTime = -1.0;

    private Action(String path, double time, boolean isTemporaryAction, String recoverPath, boolean interruptable) {
        this.path = path;
        this.time = time;
        this.isTemporaryAction = isTemporaryAction;
        this.recoverPath = recoverPath;
        this.interruptable = interruptable;
    }

    private Action(String path, double time, boolean isTemporaryAction, boolean interruptable) {
        this.path = path;
        this.time = time;
        this.isTemporaryAction = isTemporaryAction;
        this.interruptable = interruptable;
    }

    /**
     * 创建暂时的、可中断的动作
     *
     * @param path        动作路径
     * @param time        持续时间
     * @param recoverPath 恢复动作路径
     * @return 创建的动作实例
     */
    public static Action creatTemporaryInterruptableAction(String path, double time, String recoverPath) {
        return new Action(path, time, true, recoverPath, true);
    }

    /**
     * 创建持续的、可中断的动作
     *
     * @param path 动作路径
     * @return 创建的动作实例
     */
    public static Action creatContinuousInterruptableAction(String path) {
        return new Action(path, PerpetualTime, false, true);
    }

    /**
     * 创建短暂的、不可中断的动作,例如退出动画
     *
     * @param path        动作路径
     * @param time        持续时间
     * @param recoverPath 恢复动作路径
     * @return 创建的动作实例
     */
    public static Action creatTemporaryUninterruptibleAction(String path, double time, String recoverPath) {
        return new Action(path, time, true, recoverPath, false);
    }

    /**
     * 创建持续的、不可中断的动作,比较苛刻展示想不到案例
     *
     * @param path 动作路径
     * @return 创建的动作实例
     */
    public static Action creatContinuousUninterruptibleAction(String path) {
        return new Action(path, PerpetualTime, false, false);
    }

    public String getPath() {
        return path;
    }

    public double getTime() {
        return time;
    }

    public boolean isTemporaryAction() {
        return isTemporaryAction;
    }

    public String getRecoverPath() {
        return recoverPath;
    }

    public boolean isInterruptable() {
        return interruptable;
    }
}

并且采纳《Effective Java》“隐藏”了构造函数,并且提供公开的接口来构造四种类型的动作,分别是

  • creatTemporaryInterruptableAction:暂时的、可中断的动作
  • creatContinuousInterruptableAction:持续的、可中断的动作
  • creatTemporaryUninterruptibleAction:暂时的、不可中断的动作
  • creatContinuousUninterruptibleAction:持续的、不可中断的动作

动作生成者

一个动作的产生是随机的,如果放在动作类或者动作执行类不太妥当,因此将其独立管理,构建了一个名为ActionGenerator的动作生成者类

package org.taibai.hellohei.ui;

import org.taibai.hellohei.constant.Constant;

import java.util.HashMap;
import java.util.Map;

/**
 * <p>Creation Time: 2021-09-21 18:15:02</p>
 * <p>Description: 获取一个新的交互动作以及交互动作的关闭</p>
 *
 * @author 太白
 */
public class ActionGenerator {

    /**
     * 动作编号
     */
    private int actionIndex = NoAction;

    private static final Map<Integer, String> resource = new HashMap<Integer, String>() {{
        put(1, Constant.ImageShow.guitarImage);
    }};
    private static final int MinIndex = 1;
    private static final int MaxIndex = 1;
    public static final int NoAction = 0;

    /**
     * 随机生成一个动作编号,这里当动作编号不为0时说明动作还未结束
     *
     * @return 当且仅当上一个动作未结束时,返回false,且不生成新动作
     */
    public boolean generateNewActionIndex() {
        if (actionIndex != NoAction) return false;
        actionIndex = (int) (Math.random() * (MaxIndex - MinIndex + 1) + MinIndex);
        return true;
    }

    /**
     * 结束动作时必须调用该API,约定的
     *
     * @return 是否关闭,若早已关闭也返回false
     */
    public void close() {
        actionIndex = NoAction;
    }

    /**
     * 获得动作的GIF资源
     *
     * @return 动作GIF资源文件相对路径
     */
    public String getActionPath() {
        if (resource.containsKey(actionIndex))
            return resource.get(actionIndex);
        return null;
    }
}

这里约定一个动作的开启必须要关闭,不关闭将不会再生成动作。

动作执行者

动作的执行是互相影响的,例如连续点击不应该连续触发动作等,因此将其独立出来,构建了一个动作执行者类ActionExecutor

package org.taibai.hellohei.ui;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Duration;
import org.taibai.hellohei.constant.Constant;
import org.taibai.hellohei.img.ResourceGetter;

/**
 * <p>Creation Time: 2021-09-22 11:49:12</p>
 * <p>Description: 动作执行者</p>
 *
 * @author 太白
 */
public class ActionExecutor {

    private ImageView imageView;
    private Action curAction;
    private final ResourceGetter resourceGetter = ResourceGetter.newInstance();
    private final ActionGenerator actionGenerator = new ActionGenerator();
    private static ActionExecutor actionExecutor;
    private Timeline timeline;

    public static ActionExecutor newInstance(ImageView imageView) {
        if (actionExecutor == null) actionExecutor = new ActionExecutor(imageView);
        return actionExecutor;
    }

    private ActionExecutor(ImageView imageView) {
        this.imageView = imageView;
    }

    public boolean execute(Action action) {
        // 如果上一个动作不可中断,那么动作执行失败
        if (curAction != null && !curAction.isInterruptable()) return false;
        Image actionImage = resourceGetter.get(action.getPath());
        imageView.setImage(actionImage);
        curAction = action;
        if (timeline != null) timeline.pause();
        // 如果当前动作是暂时的,则还需要恢复到某一个动作
        if (action.isTemporaryAction()) {
            timeline = new Timeline(new KeyFrame(Duration.seconds(action.getTime()), e -> executeContinuousInterruptableActionAction(action.getRecoverPath())));
            timeline.play();
        }
        return true;
    }

    public boolean executeClickAction() {
        boolean ok = actionGenerator.generateNewActionIndex();
        if (ok) {
            execute(Action.creatTemporaryInterruptableAction(
                    actionGenerator.getActionPath(),
                    Constant.UserInterface.RunTime,
                    Constant.ImageShow.mainImage));
        }
        return ok;
    }

    /**
     * 立即执行一个可中断的、持续的动作
     */
    private void executeContinuousInterruptableActionAction(String path) {
        curAction = null;
        timeline = null;
        actionGenerator.close();
        Action action = Action.creatContinuousInterruptableAction(path);
        execute(action);
    }

}

动作的执行是影响全局的,因此将其设计为单例模式,这样全局拿到的就是同一个对象。所产生的影响也是全局同步的。

事件

事件起初遇到了点麻烦,就是拖动也会触发点击事件,如果在主入口程序设置会很繁琐,因此我将事件管理划到了一个类中,这样拖动时记录初始坐标,松开鼠标时只需要判断坐标值是不是一样的,如果是一样的就说明在原地,执行点击事件(就是逗小黑玩)。虽然有可能拖动到同一个地方,但用户既然要拖拽肯定是想移动一个位置,所以不大可能回到原来的位置(就算故意移回到原位也很困难是吧~)

package org.taibai.hellohei.event;

import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import org.taibai.hellohei.constant.Constant;
import org.taibai.hellohei.img.ResourceGetter;
import org.taibai.hellohei.ui.Action;
import org.taibai.hellohei.ui.ActionExecutor;
import org.taibai.hellohei.ui.ActionGenerator;

/**
 * <p>Creation Time: 2021-09-22 12:50:52</p>
 * <p>Description: 全局事件监听者</p>
 *
 * @author 太白
 */
public class GlobalEventListener {

    private final Stage stage;
    private final ImageView imageView;
    private final AnchorPane anchorPane;
    /**
     * 动作执行者,触发的动作需要托付给动作执行者执行
     */
    private final ActionExecutor actionExecutor;

    private double xOffset = 0;
    private double yOffset = 0;
    private double preScreenX = 0;
    private double preScreenY = 0;

    public GlobalEventListener(Stage stage, ImageView imageView, AnchorPane anchorPane) {
        this.stage = stage;
        this.imageView = imageView;
        this.anchorPane = anchorPane;
        this.actionExecutor = ActionExecutor.newInstance(imageView);
        enableDrag();
        enableClick();
    }

    /**
     * 激活拖动
     */
    private void enableDrag() {
        anchorPane.setOnMousePressed(e -> {
            xOffset = e.getSceneX();
            yOffset = e.getSceneY();
        });
        anchorPane.setOnMouseDragged(e -> {
            stage.setX(e.getScreenX() - xOffset);
            stage.setY(e.getScreenY() - yOffset);
        });
    }

    /**
     * 点击随机触发一个动作
     */
    private void enableClick() {
        imageView.setOnMousePressed(e -> {
            preScreenX = e.getScreenX();
            preScreenY = e.getScreenY();
        });
        imageView.setOnMouseReleased(e -> {
            if (e.getScreenX() == preScreenX && e.getScreenY() == preScreenY) {
                actionExecutor.executeClickAction();
            }
        });
    }

}

资源加载器

整个程序的运作都需要GIF图片的显示,因此需要用一个类去加载GIF图片,这里使用类级别与属性级别的单例模式,降低了创建类所需要的时间,当然如果使用HashMap容易导致内存泄漏,因此使用WeekHashMap

扩展阅读 WeekHashMap
和HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。不过WeakHashMap的键是“弱键”。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。

package org.taibai.hellohei.img;

import javafx.scene.image.Image;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.WeakHashMap;

/**
 * <p>Creation Time: 2021-09-21 18:35:46</p>
 * <p>Description: 资源加载器</p>
 *
 * @author 太白
 */
public class ResourceGetter {

    private static final Map<String, Image> images = new WeakHashMap<>();
    private static ResourceGetter singleton;

    public static ResourceGetter newInstance() {
        if (singleton == null) singleton = new ResourceGetter();
        return singleton;
    }

    private ResourceGetter() {
    }

    public Image get(String path) {
        if (!images.containsKey(path)) {
            images.put(path, new Image(Objects.requireNonNull(this.getClass().getResourceAsStream(path))));
        }
        return images.get(path);
    }

}

交互平台

目前的交互功能仅仅只有碎碎念、显示隐藏,希望后面能扩充点功能。交互平台开启一个线程,随机事件后触发一次交互功能,比如开启碎碎念功能后,将在随机事件后弹出消息框。

package org.taibai.hellohei.ui;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.taibai.hellohei.constant.Constant;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Objects;
import java.util.Random;

/**
 * <p>Creation Time: 2021-09-21 19:00:47</p>
 * <p>Description: 交互功能</p>
 *
 * @author 太白
 */
public class InterfaceFunction {

    private final ImageView imageView;
    private final ActionExecutor actionExecutor;
    private final Stage stage;
    private VBox messageBox;
    private CheckboxMenuItem itemSay = new CheckboxMenuItem("碎碎念");
    private final String greet = "好久不见鸭,想你了~";

    public InterfaceFunction(Stage stage, ImageView imageView) {
        this.stage = stage;
        this.imageView = imageView;
        this.actionExecutor = ActionExecutor.newInstance(imageView);
        this.messageBox = new VBox();
        initMessage();
        say(greet, 8);
        // 开启随机事件
        RandomEvent randomEvent = new RandomEvent();
        new Thread(randomEvent).start();
    }

    /**
     * 初始化消息框
     */
    private void initMessage() {
        Label bubble = new Label();
        //设置气泡的宽度。如果没有这句,就会根据内容多少来自适应宽度
        bubble.setPrefWidth(100);
        bubble.setWrapText(true);   //自动换行
        bubble.setStyle("-fx-background-color: rgba(255,255,255,0.7); -fx-background-radius: 8px;");
        bubble.setPadding(new Insets(7)); //标签的内边距的宽度
        bubble.setFont(new javafx.scene.text.Font(14));
        bubble.setTextFill(Color.web("#000000"));

        Polygon triangle = new Polygon(0.0, 0.0, 8.0, 10.0, 16.0, 0.0);//分别设置三角形三个顶点的X和Y
        triangle.setFill(new Color(1, 1, 1, 0.7));

        // VBox.setMargin(triangle, new Insets(0, 50, 0, 0));//设置三角形的位置,默认居中
        messageBox.getChildren().addAll(bubble, triangle);
        messageBox.setAlignment(Pos.BOTTOM_CENTER);
        messageBox.setStyle("-fx-background:transparent;");
        //设置相对于父容器的位置
        messageBox.setLayoutX(0);
        messageBox.setLayoutY(0);
        messageBox.setVisible(true);
    }

    /**
     * 退出
     */
    public void exit() {
        // 展示告别动画
        double time = 1.5;
        actionExecutor.execute(Action.creatTemporaryUninterruptibleAction(Constant.ImageShow.byeImage, time, Constant.ImageShow.mainImage));
        // 要用Platform.runLater,不然会报错Not on FX application thread;
        Platform.runLater(() -> say("再见~", Constant.UserInterface.SayingRunTime));
        // 动画结束后执行退出
        new Timeline(new KeyFrame(
                Duration.seconds(time),
                ae -> System.exit(0)))
                .play();
    }

    /**
     * 说一句话
     *
     * @param msg      消息
     * @param duration 持续时间
     */
    public void say(String msg, int duration) {
        Label lbl = (Label) messageBox.getChildren().get(0);
        lbl.setText(msg);
        messageBox.setVisible(true);
        //设置气泡的显示时间
        new Timeline(new KeyFrame(
                Duration.seconds(duration),
                ae -> {
                    messageBox.setVisible(false);
                }))
                .play();
    }

    /**
     * 添加系统托盘
     *
     * @param stage 舞台
     */
    public void setTray(Stage stage) {
        SystemTray tray = SystemTray.getSystemTray();
        //托盘图标
        BufferedImage image;
        try {
            // 为托盘添加一个右键弹出菜单
            PopupMenu popMenu = new PopupMenu();
            popMenu.setFont(new Font("微软雅黑", Font.PLAIN, 14));

            MenuItem itemShow = new MenuItem("显示");
            itemShow.addActionListener(e -> Platform.runLater(() -> stage.show()));

            MenuItem itemHide = new MenuItem("隐藏");
            // 要先setImplicitExit(false),否则stage.hide()会直接关闭stage
            // stage.hide()等同于stage.close()
            itemHide.addActionListener(e -> {
                Platform.setImplicitExit(false);
                Platform.runLater(stage::hide);
            });

            MenuItem itemExit = new MenuItem("退出");
            itemExit.addActionListener(e -> exit());

            popMenu.add(itemSay);
            popMenu.addSeparator();
            popMenu.add(itemShow);
            popMenu.add(itemHide);
            popMenu.add(itemExit);
            //设置托盘图标
            image = ImageIO.read(Objects.requireNonNull(getClass().getResourceAsStream(Constant.ImageShow.iconImage)));
            TrayIcon trayIcon = new TrayIcon(image, "小黑", popMenu);
            trayIcon.setToolTip("小黑");
            trayIcon.setImageAutoSize(true);//自动调整图片大小。这步很重要,不然显示的是空白
            tray.add(trayIcon);
        } catch (IOException | AWTException e) {
            e.printStackTrace();
        }
    }

    public ImageView getImageView() {
        return imageView;
    }

    public VBox getMessageBox() {
        return messageBox;
    }

    class RandomEvent implements Runnable {
        @Override
        public void run() {
            while (true) {
                Random rand = new Random();
                //随机发生自动事件,以下设置间隔为9~24秒。要注意这个时间间隔包含了动画播放的时间
                long time = (rand.nextInt(15) + 10) * 1000;
                if (itemSay.getState()) {
                    //随机选择要说的话。因为目前只有两个宠物,所以可以用三目运算符
                    String str = Constant.UserInterface.selfTalking[rand.nextInt(5)];
                    Platform.runLater(() -> say(str, Constant.UserInterface.SayingRunTime));
                }
                try {
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

之后功能还会继续扩充,苦命考研狗,先去学习了~


点击全文阅读


本文链接:http://zhangshiyu.com/post/32886.html

动作  交互  太白  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1