SwingTimer实现流畅动画与并发控制
文章小白一枚,正在不断学习积累知识,现将学习到的知识记录一下,也是将我的所得分享给大家!而今天这篇文章《Swing Timer实现流畅动画与并发操作》带大家来了解一下##content_title##,希望对大家的知识积累有所帮助,从而弥补自己的不足,助力实战开发!
1. Swing并发模型概述与挑战
Java Swing是一个单线程的GUI工具包,这意味着所有与用户界面组件相关的操作(包括创建、更新和绘制)都必须在特定的线程上执行,这个线程被称为事件调度线程(Event Dispatch Thread, EDT)。
直接在EDT上执行耗时操作(如文件I/O、网络请求或复杂的计算)会导致UI冻结,因为它会阻塞EDT,使其无法响应用户输入或处理重绘事件。反之,如果尝试在EDT之外的线程直接修改UI组件的状态或调用其方法,则可能导致不可预测的行为、视觉错误甚至死锁,因为Swing组件并非线程安全的。
因此,在Swing中实现动画或任何需要后台处理的功能时,必须遵循以下两个核心原则:
- UI更新必须在EDT上进行。
- 长时间运行或阻塞操作不应在EDT上进行。
2. 为什么不直接使用Thread进行UI动画?
在初学者尝试实现动画时,一个常见的误区是让动画对象(例如本例中的Circle)继承Thread并直接在run方法中更新自身位置,然后期待UI能自动重绘。原始代码中的Circle类继承了Thread,并在其run方法中通过move()更新坐标,然后调用Thread.sleep(100)。这种做法存在以下几个问题:
- UI重绘机制缺失: Circle线程虽然更新了自身坐标,但并没有通知Swing系统需要重绘UI。Swing的paint或paintComponent方法是由EDT调用的,它只会在系统认为需要重绘时才发生(例如窗口被遮挡后重新显示,或者显式调用repaint())。
- 线程安全问题: 即使Circle线程能够触发repaint(),如果repaint()在Circle更新坐标的同时被调用,就可能存在竞态条件,导致绘制出不一致或损坏的UI。
- 资源管理: 频繁创建和管理自定义线程会增加系统开销,并且容易出现线程生命周期管理不当的问题。
正确的做法是将动画逻辑(即模型状态的更新)与UI绘制逻辑(即视图的呈现)分离。动画模型的状态可以在EDT之外的线程更新(如果耗时),但触发UI重绘和实际的UI绘制必须发生在EDT上。对于周期性、轻量级的动画,javax.swing.Timer是最佳选择。
3. 使用javax.swing.Timer实现流畅动画
javax.swing.Timer是Swing提供的一个轻量级定时器,它专门设计用于在EDT上触发事件。这意味着通过Timer执行的代码将自动在EDT上运行,从而避免了线程安全问题。
以下是如何使用Swing Timer实现圆形动画的详细步骤和代码示例:
3.1 应用程序主入口 (Main类)
首先,确保Swing应用程序的启动代码在EDT上执行。这通过EventQueue.invokeLater()实现。
import java.awt.EventQueue; import javax.swing.JFrame; public class Main { public static void main(String[] args) { // 确保所有UI相关的初始化都在EDT上进行 EventQueue.invokeLater(new Runnable() { @Override public void run() { JFrame frame = new JFrame("Swing Timer 动画示例"); frame.add(new TestPane()); // 添加自定义的JPanel frame.pack(); // 根据组件的首选大小调整窗口大小 frame.setLocationRelativeTo(null); // 窗口居中 frame.setVisible(true); // 显示窗口 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 设置关闭操作 } }); } }
3.2 绘制面板 (TestPane类)
TestPane是一个JPanel的子类,它负责管理动画对象(Circle实例)的集合,并通过Swing Timer驱动动画逻辑和UI重绘。
import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.List; import javax.swing.JPanel; import javax.swing.Timer; // 注意是javax.swing.Timer public class TestPane extends JPanel { private List<Circle> circles = new ArrayList<>(); private Timer timer; // Swing Timer实例 public TestPane() { // 初始化圆形对象 circles.add(new Circle(100, 100, 2, 2, new int[]{50, 50, 250, 250}, Color.blue)); // 修正边界 circles.add(new Circle(150, 150, -2, 0, new int[]{50, 50, 250, 250}, Color.red)); circles.add(new Circle(50, 150, 2, -2, new int[]{50, 50, 250, 250}, Color.green)); } @Override public Dimension getPreferredSize() { // 定义面板的首选大小 return new Dimension(300, 300); } /** * 当组件被添加到可显示层次结构时调用。 * 在这里启动Timer,确保组件可见时动画开始。 */ @Override public void addNotify() { super.addNotify(); if (timer != null) { timer.stop(); // 停止旧的Timer以防万一 } // 创建并启动Timer // 5毫秒间隔,ActionListener将在EDT上执行 timer = new Timer(5, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // 遍历所有圆形,更新它们的状态 for (Circle circle : circles) { circle.tick(); } repaint(); // 请求重绘面板,这也会在EDT上执行 } }); timer.start(); // 启动Timer } /** * 当组件从可显示层次结构中移除时调用。 * 在这里停止Timer,释放资源。 */ @Override public void removeNotify() { super.removeNotify(); if (timer != null) { timer.stop(); // 停止Timer } timer = null; // 清除引用 } /** * 自定义绘制方法。 * 推荐在JComponent子类中使用paintComponent而不是paint。 */ @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 必须调用父类的paintComponent方法 Graphics2D g2d = (Graphics2D) g.create(); // 创建Graphics副本,安全操作 try { for (Circle circle : circles) { g2d.setColor(circle.getColor()); g2d.fillOval(circle.getX(), circle.getY(), 10, 10); // 绘制圆形 } } finally { g2d.dispose(); // 释放Graphics资源 } } }
3.3 动画模型 (Circle类)
Circle类现在是一个纯粹的模型类,它不再继承Thread。它只负责维护自身的状态(位置、速度、颜色、边界)以及更新这些状态的逻辑。
import java.awt.Color; public class Circle { private int x, y; private int xs, ys; // x和y方向的速度 private Color color; private int[] boundries; // 边界:[minX, minY, maxX, maxY] public Circle(int x, int y, int xs, int ys, int[] boundries, Color color) { this.x = x; this.y = y; this.xs = xs; this.ys = ys; this.boundries = boundries; this.color = color; } /** * 更新圆形状态的“一帧”。 * 由Swing Timer的ActionListener调用。 */ public void tick() { move(); } /** * 更新圆形位置。 */ protected void move() { updateSpeed(); // 先检查并更新速度方向 this.x += this.xs; this.y += this.ys; } /** * 检查边界碰撞并反转速度方向。 * 修正了原始代码中的边界判断逻辑。 */ protected void updateSpeed() { // X轴边界检查 if (x <= boundries[0]) { // 触及左边界 xs *= -1; x = boundries[0]; // 将位置校正到边界上 } else if (x >= boundries[2]) { // 触及右边界 xs *= -1; x = boundries[2]; // 将位置校正到边界上 } // Y轴边界检查 if (y <= boundries[1]) { // 触及上边界 ys *= -1; y = boundries[1]; // 将位置校正到边界上 } else if (y >= boundries[3]) { // 触及下边界 ys *= -1; y = boundries[3]; // 将位置校正到边界上 } } // Getter方法 public int getX() { return x; } public Color getColor() { return color; } public int getY() { return y; } }
4. 关键概念与最佳实践
- EDT(Event Dispatch Thread): 始终记住所有UI操作都必须在EDT上执行。EventQueue.invokeLater()用于将任务提交到EDT队列。
- javax.swing.Timer: 它是实现Swing动画和周期性任务的首选工具。它的ActionListener回调总是在EDT上执行。不要与java.util.Timer混淆,后者在单独的线程上运行,不适合直接操作UI。
- paintComponent vs paint: 在自定义绘制时,应重写JComponent的paintComponent(Graphics g)方法,而不是paint(Graphics g)。paintComponent是双缓冲友好的,并且负责绘制组件的背景、边框和内容。
- super.paintComponent(g): 在自定义paintComponent方法中,始终首先调用super.paintComponent(g),以确保组件的背景被正确清除,避免绘制残影。
- Graphics上下文管理: 当你从paintComponent接收到Graphics对象时,最好创建一个副本(Graphics2D g2d = (Graphics2D) g.create();),并在完成绘制后调用g2d.dispose()来释放资源。这有助于防止意外修改原始Graphics对象的状态,并确保系统资源得到有效管理。
- 组件生命周期方法 (addNotify, removeNotify): 这些方法在组件被添加到或从其父容器中移除时调用,是管理资源(如Timer)生命周期的好地方。在addNotify中启动Timer,在removeNotify中停止Timer,可以确保动画只在组件可见且需要时运行。
- 边界碰撞逻辑: 原始代码中的边界判断 if(x
boundries[2]) 逻辑上是错误的,因为一个值不能同时小于最小值又大于最大值。正确的逻辑应该是检查是否触及或越过任一边界,然后反转速度并校正位置。
5. 总结
在Java Swing中实现动画和并发操作,核心在于理解并遵循Swing的单线程模型。通过利用EventQueue.invokeLater()确保UI初始化在EDT上,并使用javax.swing.Timer安全地驱动动画逻辑和UI重绘,我们可以避免常见的线程问题,构建出响应迅速、视觉流畅的Swing应用程序。将动画模型(如Circle)与UI绘制逻辑(如TestPane的paintComponent)清晰分离,是构建可维护和可扩展Swing应用的关键。
本篇关于《SwingTimer实现流畅动画与并发控制》的介绍就到此结束啦,但是学无止境,想要了解学习更多关于文章的相关知识,请关注golang学习网公众号!

- 上一篇
- 印象笔记视频插入与播放教程

- 下一篇
- 学习通静音设置教程详解
-
- 文章 · java教程 | 52分钟前 |
- Java高效统计列表重复元素及数值累加教程
- 310浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java非贪婪匹配替换技巧
- 478浏览 收藏
-
- 文章 · java教程 | 2小时前 |
- SpringBoot医疗系统实体关系与安全实现解析
- 141浏览 收藏
-
- 文章 · java教程 | 10小时前 |
- AndroidPlurals使用指南与实例解析
- 359浏览 收藏
-
- 文章 · java教程 | 10小时前 |
- Java泛型CSV转对象方法解析
- 264浏览 收藏
-
- 文章 · java教程 | 10小时前 |
- Java发送邮件报530认证错误怎么解决
- 179浏览 收藏
-
- 文章 · java教程 | 11小时前 | java java使用
- Java字符串拼接技巧:+连接变量与字符串
- 159浏览 收藏
-
- 文章 · java教程 | 11小时前 |
- Java随机数生成与统计分析实验
- 238浏览 收藏
-
- 文章 · java教程 | 11小时前 |
- SpringBoot整合ActiveMQ配置详解
- 320浏览 收藏
-
- 文章 · java教程 | 11小时前 |
- Spring Bean生命周期详解
- 377浏览 收藏
-
- 文章 · java教程 | 12小时前 |
- JavaWebSocket心跳检测实现教程
- 268浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 514次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 499次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- AI Mermaid流程图
- SEO AI Mermaid 流程图工具:基于 Mermaid 语法,AI 辅助,自然语言生成流程图,提升可视化创作效率,适用于开发者、产品经理、教育工作者。
- 438次使用
-
- 搜获客【笔记生成器】
- 搜获客笔记生成器,国内首个聚焦小红书医美垂类的AI文案工具。1500万爆款文案库,行业专属算法,助您高效创作合规、引流的医美笔记,提升运营效率,引爆小红书流量!
- 426次使用
-
- iTerms
- iTerms是一款专业的一站式法律AI工作台,提供AI合同审查、AI合同起草及AI法律问答服务。通过智能问答、深度思考与联网检索,助您高效检索法律法规与司法判例,告别传统模板,实现合同一键起草与在线编辑,大幅提升法律事务处理效率。
- 454次使用
-
- TokenPony
- TokenPony是讯盟科技旗下的AI大模型聚合API平台。通过统一接口接入DeepSeek、Kimi、Qwen等主流模型,支持1024K超长上下文,实现零配置、免部署、极速响应与高性价比的AI应用开发,助力专业用户轻松构建智能服务。
- 464次使用
-
- 迅捷AIPPT
- 迅捷AIPPT是一款高效AI智能PPT生成软件,一键智能生成精美演示文稿。内置海量专业模板、多样风格,支持自定义大纲,助您轻松制作高质量PPT,大幅节省时间。
- 427次使用
-
- 提升Java功能开发效率的有力工具:微服务架构
- 2023-10-06 501浏览
-
- 掌握Java海康SDK二次开发的必备技巧
- 2023-10-01 501浏览
-
- 如何使用java实现桶排序算法
- 2023-10-03 501浏览
-
- Java开发实战经验:如何优化开发逻辑
- 2023-10-31 501浏览
-
- 如何使用Java中的Math.max()方法比较两个数的大小?
- 2023-11-18 501浏览