12.5 Java7新增的Swing功能 12.5.1 使用JLayer装饰组件

12.5 Java7新增的Swing功能

Java7提供的重大更新就包括了对Swing的更新,对Swing的更新除前面介绍的Nimbus外观、改进的JColorchooser组件之外,还有两个很有用的更新:JLayer创建不规则窗口。下面将会详细介绍这两个知识点。

12.5.1 使用JLayer装饰组件

JLayer的功能是在指定组件上额外地添加一个装饰层,开发者可以在这个装饰层上进行任意绘制(直接重写paint(Graphics g,JComponent c)方法),这样就可以为指定组件添加任意装饰。
JLayer一般总是要和LayerUI一起使用,而LayerUI用于被扩展,扩展LayerUI时重写它的paint(Graphics g,JComponent c)方法,在该方法中绘制的内容会对指定组件进行装饰。
实际上,使用JLayer很简单,只要如下两行代码即可

1
2
3
4
//创建LayerUI对象
LayerUI<JComponent> layerUI = new XxxLayerUI();
//使用layerUI来装饰指定的JPanel组件
JLayer<JComponent> layer = new JLayer<JComponent>(panel, layerUI);

上面程序中的XxxLayerUI就是开发者自己扩展的子类,这个子类会重写paint(Graphics g,JComponent c)方法,重写该方法来完成“装饰层”的绘制。
上面第二行代码中的panel组件就是被装饰的组件,接下来把layer对象(layer对象包含了被装饰对象和LayerUI对象)添加到指定容器中即可

程序 蒙版效果

下面程序示范了使用JLayer为窗口添加一层“蒙版”的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class FirstLayerUI extends LayerUI<JComponent> {
private static final long serialVersionUID = 5993086002698144927L;

public void paint(Graphics g, JComponent c) {
super.paint(g, c);
Graphics2D g2 = (Graphics2D) g.create();
// 设置透明效果
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f));
// 使用渐变画笔绘图
g2.setPaint(new GradientPaint(0, 0, Color.RED, 0, c.getHeight(), Color.BLUE));
// 绘制一个与被装饰组件相同大小的矩形
g2.fillRect(0, 0, c.getWidth(), c.getHeight()); // ①
g2.dispose();
}
}

程序 主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class JLayerTest {
public void init() {
JFrame f = new JFrame("JLayer测试");
JPanel p = new JPanel();
ButtonGroup group = new ButtonGroup();
JRadioButton radioButton;
// 创建3个RadioButton,并将它们添加成一组
p.add(radioButton = new JRadioButton("网购购买", true));
group.add(radioButton);
p.add(radioButton = new JRadioButton("书店购买"));
group.add(radioButton);
p.add(radioButton = new JRadioButton("图书馆借阅"));
group.add(radioButton);
// 添加3个JCheckBox
p.add(new JCheckBox("疯狂Java讲义"));
p.add(new JCheckBox("疯狂Android讲义"));
p.add(new JCheckBox("疯狂Ajax讲义"));
p.add(new JCheckBox("轻量级Java EE企业应用"));
JButton orderButton = new JButton("投票");
p.add(orderButton);
// 创建LayerUI对象
// LayerUI<JComponent> layerUI = new SpotlightLayerUI(); // ②
// LayerUI<JComponent> layerUI = new FirstLayerUI(); // ②
LayerUI<JComponent> layerUI = new BlurLayerUI(); // ②
// 使用layerUI来装饰指定的JPanel组件
JLayer<JComponent> layer = new JLayer<JComponent>(p, layerUI);
// 将装饰后的JPanel组件添加到容器中
f.add(layer);
f.setSize(300, 170);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
}

public static void main(String[] args) {
new JLayerTest().init();
}
}

上面程序中开发了一个FirstLayerUI,它扩展了LayerUI,重写paint(graphics g, JComponent c)方法时绘制了一个半透明的、与被装饰组件具有相同大小的矩形。接下来在main方法中使用这个LayerUI来装饰指定的JPanel组件,并把JLayer添加到JFrame容器中,这就达到了对JPanel进行包装的效果。
运行该程序,可以看到如图12.23所示的效果
这里有一张图片
由于开发者可以重写paint(graphics g, JComponent c)方法,因此获得对被装饰层的全部控制权——想怎么绘制,就怎么绘制!因此开发者可以“随心所欲”地对指定组件进行装饰。

程序 模糊效果

例如,下面提供的LayerUI则可以为被装饰组件增加“模糊”效果。程序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class BlurLayerUI extends LayerUI<JComponent> {
private static final long serialVersionUID = -2386080677384858056L;
private BufferedImage screenBlurImage;
private BufferedImageOp operation;

public BlurLayerUI() {
float ninth = 1.0f / 9.0f;
// 定义模糊参数
float[] blurKernel = { ninth, ninth, ninth, ninth, ninth, ninth, ninth, ninth, ninth };
// ConvolveOp代表一个模糊处理,它将原图片的每一个像素与周围
// 像素的颜色进行混合,从而计算出当前像素的颜色值,
operation = new ConvolveOp(new Kernel(3, 3, blurKernel), ConvolveOp.EDGE_NO_OP, null);
}

public void paint(Graphics g, JComponent c) {
int w = c.getWidth();
int h = c.getHeight();
// 如果被装饰窗口大小为0X0,直接返回
if (w == 0 || h == 0)
return;
// 如果screenBlurImage没有初始化,或它的尺寸不对。
if (screenBlurImage == null || screenBlurImage.getWidth() != w || screenBlurImage.getHeight() != h) {
// 重新创建新的BufferdImage
screenBlurImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
}
Graphics2D ig2 = screenBlurImage.createGraphics();
// 把被装饰组件的界面绘制到当前screenBlurImage上
ig2.setClip(g.getClip());
super.paint(ig2, c);
ig2.dispose();
Graphics2D g2 = (Graphics2D) g;
// 对JLayer装饰的组件进行模糊处理
g2.drawImage(screenBlurImage, operation, 0, 0);
}
}

上面程序扩展了LayerUI,重写了paint(Graphics g,JComponent c)方法,重写该方法时也是绘制了一个与被装饰组件具有相同大小的矩形,只是这种绘制添加了模糊效果。

JLayerTestJava中的②号粗体字代码改为使用BlurLayerUI,再次运行该程序,将可以看到如图12.24所示的“毛玻璃”窗口
这里有一张图片

给LayerUI增加事件机制

除此之外,开发者自定义的LayerUI还可以增加事件机制,这种事件机制能让装饰层响应用户动作,随着用户动作动态地改变LayerUI上的绘制效果。比如下面的LayerUI示例,

程序 探照灯效果

程序通过响应鼠标事件可以在窗口上增加“探照灯”效果。程序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class SpotlightLayerUI extends LayerUI<JComponent> {
private static final long serialVersionUID = -5931810232610714874L;
private boolean active;
private int cx, cy;

public void installUI(JComponent c) {
super.installUI(c);
JLayer layer = (JLayer) c;
// 设置JLayer可以响应鼠标、鼠标动作事件
layer.setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
}

public void uninstallUI(JComponent c) {
JLayer layer = (JLayer) c;
// 设置JLayer不响应任何事件
layer.setLayerEventMask(0);
super.uninstallUI(c);
}

public void paint(Graphics g, JComponent c) {
Graphics2D g2 = (Graphics2D) g.create();
super.paint(g2, c);
// 如果处于激活状态
if (active) {
// 定义一个cx、cy位置的点
Point2D center = new Point2D.Float(cx, cy);
float radius = 72;
float[] dist = { 0.0f, 1.0f };
Color[] colors = { Color.YELLOW, Color.BLACK };
// 以center为中心、colors为颜色数组创建环形渐变
RadialGradientPaint p = new RadialGradientPaint(center, radius, dist, colors);
g2.setPaint(p);
// 设置渐变效果
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .6f));
// 绘制矩形
g2.fillRect(0, 0, c.getWidth(), c.getHeight());
}
g2.dispose();
}

// 处理鼠标事件的方法
public void processMouseEvent(MouseEvent e, JLayer layer) {
if (e.getID() == MouseEvent.MOUSE_ENTERED)
active = true;
if (e.getID() == MouseEvent.MOUSE_EXITED)
active = false;
layer.repaint();
}

// 处理鼠标动作事件的方法
public void processMouseMotionEvent(MouseEvent e, JLayer layer) {
Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), layer);
// 获取鼠标动作事件的发生点的坐标
cx = p.x;
cy = p.y;
layer.repaint();
}
}

上面程序中重写了LayerUIinstallUI(JComponent c)方法,重写该方法时控制该组件能响应鼠标事件和鼠标动作事件。
接下来程序
重写processMouseMotionEvent()方法,该方法负责为LayerUI上的鼠标事件提供响应:当鼠标在界面上移动时,程序会改变cxcy的坐标值
重写paint(Graphics g, JComponent c)方法时会在cxey对应的点绘制一个环形渐变,这就可以充当“探照灯”效果了。

JLayerTestJava中的②号粗体字代码改为使用SpotlightLayerUI,再次运行该程序,即可看到如图12.25所示的效果。
这里有一张图片

绘制动画

既然可以让LayerUI上的绘制效果响应鼠标动作,当然也可以在LayerUI上绘制“动画”,所谓动画,就是通过定时器控制LayerUI上绘制的图形动态地改变即可。

接下来重写的LayerUI使用了Timer来定时地改变LayerUI上的绘制,程序绘制了一个旋转中的“齿轮”,这个旋转的齿轮可以提醒用户“程序正在处理中”

程序 转动的齿轮

下面程序重写LayerUl时绘制了12条辐射状的线条,并通过Timer来不断地改变这12条线条的排列角度,这样就可以形成“转动的齿轮”了。
程序提供的WaitingLayerUI类代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class WaitingLayerUI extends LayerUI<JComponent> {
private static final long serialVersionUID = -443302447858652985L;
private boolean isRunning;
private Timer timer;
// 记录转过的角度
private int angle; // ①

public void paint(Graphics g, JComponent c) {
super.paint(g, c);
int w = c.getWidth();
int h = c.getHeight();
// 已经停止运行,直接返回
if (!isRunning)
return;
Graphics2D g2 = (Graphics2D) g.create();
Composite urComposite = g2.getComposite();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f));
// 填充矩形
g2.fillRect(0, 0, w, h);
g2.setComposite(urComposite);
// -----下面代码开始绘制转动中的“齿轮”----
// 计算得到宽、高中较小值的1/5
int s = Math.min(w, h) / 5;
int cx = w / 2;
int cy = h / 2;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 设置笔触
g2.setStroke(new BasicStroke(s / 2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2.setPaint(Color.BLUE);
// 画笔绕被装饰组件的中心转过angle度
g2.rotate(Math.PI * angle / 180, cx, cy);
// 循环绘制12条线条,形成“齿轮”
for (int i = 0; i < 12; i++) {
float scale = (11.0f - (float) i) / 11.0f;
g2.drawLine(cx + s, cy, cx + s * 2, cy);
g2.rotate(-Math.PI / 6, cx, cy);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, scale));
}
g2.dispose();
}

// 控制等待(齿轮开始转动)的方法
public void start() {
// 如果已经在运行中,直接返回
if (isRunning)
return;
isRunning = true;
// 每隔0.1秒重绘一次
timer = new Timer(100, e -> {
if (isRunning) {
// 触发applyPropertyChange()方法,让JLayer重绘。
// 在这行代码中,后面两个参数没有意义。
firePropertyChange("crazyitFlag", 0, 1);
// 角度加6
angle += 6; // ②
// 到达360后再从0开始
if (angle >= 360)
angle = 0;
}
});
timer.start();
}

// 控制停止等待(齿轮停止转动)的方法
public void stop() {
isRunning = false;
// 最后通知JLayer重绘一次,清除曾经绘制的图形
firePropertyChange("crazyitFlag", 0, 1);
timer.stop();
}

public void applyPropertyChange(PropertyChangeEvent pce, JLayer layer) {
// 控制JLayer重绘
if (pce.getPropertyName().equals("crazyitFlag")) {
layer.repaint();
}
}
}

程序 主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class WaitingJLayerTest {
public void init() {
JFrame f = new JFrame("转动的“齿轮”");
JPanel p = new JPanel();
ButtonGroup group = new ButtonGroup();
JRadioButton radioButton;
// 创建3个RadioButton,并将它们添加成一组
p.add(radioButton = new JRadioButton("网购购买", true));
group.add(radioButton);
p.add(radioButton = new JRadioButton("书店购买"));
group.add(radioButton);
p.add(radioButton = new JRadioButton("图书馆借阅"));
group.add(radioButton);
// 添加3个JCheckBox
p.add(new JCheckBox("疯狂Java讲义"));
p.add(new JCheckBox("疯狂Android讲义"));
p.add(new JCheckBox("疯狂Ajax讲义"));
p.add(new JCheckBox("轻量级Java EE企业应用"));
JButton orderButton = new JButton("投票");
p.add(orderButton);
// 创建LayerUI对象
final WaitingLayerUI layerUI = new WaitingLayerUI();
// 使用layerUI来装饰指定JPanel组件
JLayer<JComponent> layer = new JLayer<JComponent>(p, layerUI);
// 设置4秒之后执行指定动作:调用layerUI的stop()方法
final Timer stopper = new Timer(4000, ae -> layerUI.stop());
// 设置stopper定时器只触发一次。
stopper.setRepeats(false);
// 为orderButton绑定事件监听器:单击该按钮时:调用layerUI的start()方法
orderButton.addActionListener(ae -> {
layerUI.start();
// 如果stopper定时器已停止,启动它
if (!stopper.isRunning()) {
stopper.start();
}
});
// 将装饰后的JPanel组件添加到容器中
f.add(layer);
f.setSize(300, 170);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
}

public static void main(String[] args) {
new WaitingJLayerTest().init();
}
}

上面程序中的①号粗体字代码定义了一个angle变量,它负责控制12条线条的旋转角度。程序使用Timer定时地改变angle变量的值(每隔0.1秒angle加6),如③号粗体字代码所示。控制了angle角度之后,程序根据该angle角度绘制12条线条,如②号粗体字代码所示。
提供了WaitingLayerUI之后,接下来使用该WaitingLayerUI与使用前面的UI没有任何区别。不过程序需要通过特定事件来显示WaitingLayerUI的绘制(就是调用它的start方法),下面程序为按钮添加了事件监听器:当用户单击该按钮时,程序会调用WaitingLayerUI对象的start方法。

1
2
3
4
5
6
7
8
// 为orderButton绑定事件监听器:单击该按钮时:调用layerUI的start()方法
orderButton.addActionListener(ae -> {
layerUI.start();
// 如果stopper定时器已停止,启动它
if (!stopper.isRunning()) {
stopper.start();
}
});

除此之外,上面代码中还用到了stopper计时器,它会控制在一段时间(比如4秒)之后停止绘制WaitingLayerUI,因此程序还通过如下代码进行控制

1
2
3
4
// 设置4秒之后执行指定动作:调用layerUI的stop()方法
final Timer stopper = new Timer(4000, ae -> layerUI.stop());
// 设置stopper定时器只触发一次。
stopper.setRepeats(false);

再次运行该程序,可以看到如图12.26所示的“动画装饰”效果。
这里有一张图片
通过上面几个例子可以看出,Swing提供的JLayer为窗口美化提供了无限可能性。只要你想做的,比如希望用户完成输入之后,立即在后面显示一个简单的提示按钮(钩表示输入正确,叉表示输入错误)……都可以通过JLayer绘制。