Java2D API是Java中的一种图形绘制接口,它能够让开发者创建和在屏幕上显示2D图形、图像、文本等图元。这篇笔记我们对Java2D绘图进行介绍。
Java2D API中绘制图像主要使用Graphics2D
类,它类似于画笔提供了各种绘图方法。我们这里使用扩展JComponent
的方式,介绍如何使用Graphics2D绘制2D图形,一个基础的代码框架如下。
package com.gacfox.demo;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JComponent;
class MyComponent extends JComponent {
@Override
protected void paintComponent(Graphics g) {
Graphics2D graphics2D = (Graphics2D) g;
// 绘制我们的图形
// ...
}
@Override
public Dimension getPreferredSize() {
return new Dimension(640, 480);
}
}
我们重写了JComponent
的绘制paintComponent
和获得预定义大小getPreferredSize
函数,外层将MyComponent
放入JFrame
中即可。我们要进行实验的就是Graphics2D
这个类。Graphics2D库采用面向对象的方式将各种几何图形组织了起来,常用的包括Line2D
直线、Rectangle2D
矩形、Ellipse2D
椭圆等,他们实现了都Shape
接口。
下面例子代码实现了绘制矩形。
Rectangle2D rectangle2D = new Rectangle2D.Double(20, 20, 100, 100);
graphics2D.draw(rectangle2D);
Rectangle2D
构造函数传入坐标和长宽,调用draw()
即可绘制。
注意Rectangle2D.Double
,实际上还有一个Rectangle2D.Float
,这两个画的都是矩形,但是我们知道绘图时一般都使用单精度浮点数,Graphics2D也是如此,但是这就带来了类型转换的麻烦,因此Graphics2D提供了double版的Rectangle2D
供我们使用,类型转换在其内部实现了。
运行结果如下图所示。
下面例子代码实现了绘制圆形。
Ellipse2D ellipse2D = new Ellipse2D.Double(20, 20, 100, 100);
graphics2D.draw(ellipse2D);
构造函数传入的参数和矩形完全相同,只不过绘制的是矩形的内切圆。
运行结果如下图所示。
下面例子代码实现了绘制直线。
Line2D line2D = new Line2D.Double(20, 20, 250, 300);
graphics2D.draw(line2D);
传入参数为起始点和终止点坐标。
运行结果如下图所示。
当然,Rectangle2D
等对象都提供了若干get
方法获取其关键的属性,这里就不全部介绍了。
graphics2D.drawString("你好世界!", 100, 100);
我们使用drawString
函数,参数传入文字和坐标。
运行结果如下图所示。
Font font = new Font("Sans", Font.BOLD, 20);
graphics2D.setFont(font);
graphics2D.drawString("你好,世界!", 20, 20);
我们可以直接指定字体名称,或者从流加载。
运行结果如下图所示。
Graphics2D预定义了一系列颜色,例如Color.RED
等,我们也可以实例化Color对象,定义自己的颜色。
graphics2D.setPaint(Color.blue);
graphics2D.drawString("你好,世界!", 100, 100);
graphics2D.setPaint(new Color(255, 0, 0));
Ellipse2D ellipse2D = new Ellipse2D.Double(200, 200, 20, 50);
graphics2D.fill(ellipse2D);
setPaint()
函数可以设置一个画笔颜色,注意:这个函数的参数为Paint
,但是Color
类实现了Paint
。设置画笔颜色后,绘制的图形就是新设置的颜色了。
fill()
函数传入一个Shape
类型的参数,会为这个图形指定填充颜色。
运行结果如下图所示。
如果我们继承的是JComponent
,绘制背景颜色时就不得不自己画一个大矩形充当背景了,如果我们继承JPanel
,可以直接对JPanel对象设置背景颜色。
void setBackground(java.awt.Color bg)
这里要注意,如果覆盖了JPanel
的paintComponent
方法,方法里要调用super.paintComponent(g)
才能让背景颜色设置生效。
绘制图像需要一个Image
对象。
URL iconUrl = getClass().getResource("/icon.png");
ImageIcon icon = new ImageIcon(iconUrl);
graphics2D.drawImage(icon.getImage(), 10, 10, null);
运行结果如下图所示。
上面我们绘制的都是静态图像,实际上我们也能实现帧动画的效果。虽然Swing是一个事件驱动的GUI框架,但实际上我们也可以单独开一个线程,实现类似游戏引擎的帧循环。Swing组件默认的绘制方法实现了双缓冲绘制,因此我们实现帧动画其实非常简单,下面例子我们绘制了一个来回移动的小球,并绘制显示了当前帧数。
package com.gacfox.demo;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Ellipse2D;
public class MyComponent extends JComponent {
private volatile boolean frameLoop;
private volatile long lastFrameTimeMillis = 0;
private static int MIN_FRAME_INTERVAL = 16;
private int direction = 1;
private int posX = 0;
private int posY = 0;
private long currentFrameTimer = 0;
private int currentFrame = -1;
public MyComponent() {
super();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(640, 50);
}
public void init() {
// 开启帧循环线程,循环周期为MIN_FRAME_INTERVAL
frameLoop = true;
Thread frameThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (frameLoop) {
long currentTimeMillis = System.currentTimeMillis();
final long deltaTime = currentTimeMillis
- lastFrameTimeMillis;
if (deltaTime >= MIN_FRAME_INTERVAL) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
update(deltaTime);
}
});
lastFrameTimeMillis = System.currentTimeMillis();
} else {
try {
long sleepMillis = MIN_FRAME_INTERVAL
- (currentTimeMillis - lastFrameTimeMillis);
Thread.sleep(sleepMillis > 0 ? sleepMillis : 0);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
}
}
}
});
frameThread.start();
}
public void destroy() {
frameLoop = false;
}
private void update(long deltaTime) {
// 计算小球方向和位置
if (posX >= 590) {
direction = 0;
}
if (posX <= 0) {
direction = 1;
}
if (direction == 1) {
posX = posX + 1;
} else {
posX = posX - 1;
}
// 绘制当前帧数,每500ms更新
currentFrameTimer = currentFrameTimer + deltaTime;
if (currentFrameTimer > 500) {
if (deltaTime <= 0) {
currentFrame = -1;
} else {
currentFrame = (int) (1000 / deltaTime);
}
currentFrameTimer = 0;
}
// 重绘界面
this.revalidate();
this.repaint();
}
@Override
protected void paintComponent(Graphics g) {
// 绘制小球
Graphics2D graphics2D = (Graphics2D) g;
Ellipse2D ellipse2D = new Ellipse2D.Double(posX, posY, 50, 50);
graphics2D.draw(ellipse2D);
// 绘制文字
Font font = new Font("Sans", Font.PLAIN, 14);
graphics2D.setFont(font);
String currentFrameStr = "∞";
if (currentFrame > 0) {
currentFrameStr = String.valueOf(currentFrame);
}
graphics2D.drawString("当前帧数:" + currentFrameStr, 14, 14);
}
}
运行结果如下图所示。