Wednesday 2 December 2009

Radial Gradient JXLayer LockableUI

Every now and again I climb out of system programming and delve into a bit of GUI work. No matter how good the system is, if the GUI looks bad, users will think that the system is bad. I know a lot of firms don't see this as a priority but I really do. Even internal systems should look good and if you know what you are doing, Swing can be your friend.

JXLayer is a library that allows you to do various things, such as blocking input to the user interface as well drawing over components. It's a bit like the built in glass pane but a lot more powerful.

When something bad happens in my system, I have a component that I want to block all user input to as well as making it obvious to the user that something has gone wrong. JXLayer is a good choice here for blocking user input but choosing what to display was a bit more difficult. There are a few built in or easy to generate LockableUI's (see the "Getting started" section on the home page) but I wanted something different.

I wanted something that was semi transparent in the centre of the screen, fading towards the edges. It was pretty easy to accomplish this using RadialGradientPaint.

This is how a demo app (can you tell it doesn't really do anything?) looks before it is blocked:


and when I click the button to simulate that there is a problem I get this:


Here is the code that accomplishes this:

import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.ext.LockableUI;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;

public class MainFrame extends JFrame {
public MainFrame() {
super("JXLayer Demo");
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setPreferredSize(new Dimension(1000, 800));
JPanel mainPanel = new JPanel(new FlowLayout());
for (int i = 0; i < 45; i++) {
mainPanel.add(new JTextField(20));
mainPanel.add(new JButton("Press ME"));
mainPanel.add(new JLabel("This is a label"));
}
final RadialLockableUI lockableUI = new RadialLockableUI();
final JXLayer layer = new JXLayer(mainPanel, lockableUI);
add(layer);
JButton button = new JButton("Lock / Unlock");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
lockableUI.setLocked(!lockableUI.isLocked());
}
});
add(button, BorderLayout.SOUTH);
pack();
setLocationRelativeTo(null);
setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new MainFrame();
}
});
}

private static class RadialLockableUI extends LockableUI {
@Override
protected void paintLayer(Graphics2D g2, JXLayer layer) {
super.paintLayer(g2, layer);
if (isLocked()) {
int width = layer.getWidth();
int height = layer.getHeight();
Paint origPaint = g2.getPaint();
g2.setPaint(new RadialGradientPaint(new Point2D.Double(width / 2, width / 2),
width * 0.75f,
new Point2D.Double(width / 2.0, width / 2.0),
new float[] {0.0f, 0.7f, 1.0f},
new Color[] {new Color(255,0,0,128), new Color(255,0,0,64), new Color(255,0,0,0)},
MultipleGradientPaint.CycleMethod.NO_CYCLE,
MultipleGradientPaint.ColorSpaceType.SRGB,
AffineTransform.getScaleInstance(1.0, (double)height / (double)width)));
g2.fillRect(0, 0, width, height);
g2.setPaint(origPaint);
}
}
}
}
Now say that you wanted to have some text across the middle of the screen so it looks a bit like this:


You would do it with the following code:

import org.jdesktop.jxlayer.JXLayer;
import org.jdesktop.jxlayer.plaf.ext.LockableUI;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;

public class MainFrame2 extends JFrame {
public MainFrame2() {
super("JXLayer Demo");
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setPreferredSize(new Dimension(1000, 800));
JPanel mainPanel = new JPanel(new FlowLayout());
for (int i = 0; i < 45; i++) {
mainPanel.add(new JTextField(20));
mainPanel.add(new JButton("Press ME"));
mainPanel.add(new JLabel("This is a label"));
}
final RadialLockableUI lockableUI = new RadialLockableUI();
final JXLayer layer = new JXLayer(mainPanel, lockableUI);
add(layer);
JButton button = new JButton("Lock / Unlock");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
lockableUI.setLocked(!lockableUI.isLocked());
}
});
add(button, BorderLayout.SOUTH);
pack();
setLocationRelativeTo(null);
setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new MainFrame2();
}
});
}

private static class RadialLockableUI extends LockableUI {
@Override
protected void paintLayer(Graphics2D g2, JXLayer layer) {
super.paintLayer(g2, layer);
if (isLocked()) {
int width = layer.getWidth();
int height = layer.getHeight();
Paint origPaint = g2.getPaint();
g2.setPaint(new RadialGradientPaint(new Point2D.Double(width / 2, width / 2),
width * 0.75f,
new Point2D.Double(width / 2.0, width / 2.0),
new float[] {0.0f, 0.7f, 1.0f},
new Color[] {new Color(255,0,0,128), new Color(255,0,0,64), new Color(255,0,0,0)},
MultipleGradientPaint.CycleMethod.NO_CYCLE,
MultipleGradientPaint.ColorSpaceType.SRGB,
AffineTransform.getScaleInstance(1.0, (double)height / (double)width)));
g2.fillRect(0, 0, width, height);
g2.setPaint(origPaint);

g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Font origFont = g2.getFont();
float textSize = 1.0f;
Font lastFont = origFont.deriveFont(textSize);
Font fontToUse = lastFont;
int textWidth = 0;
String text = "CODE DISCONNECTED";
while (textWidth < width) {
textSize += 1.0f;
fontToUse = lastFont;
lastFont = lastFont.deriveFont(textSize);
textWidth = g2.getFontMetrics(lastFont).stringWidth(text);
}

g2.setFont(fontToUse);
FontMetrics metrics = g2.getFontMetrics();
textWidth = metrics.stringWidth(text);
int x = (width - textWidth) / 2;
int y = (metrics.getAscent() + (height - (metrics.getAscent() + metrics.getDescent())) / 3);
g2.drawString(text, x, y);
}
}
}
}
Most of the additional code here is just figuring out what size the text should be. If you want the text to change over time, just extract the text component out into a field, set it and then call repaint on the JXLayer (layer in the example above). One thing that is vital to do is to call setDirty(true) (this is a protected method so has to be done in the subclass of the layer (RadialLockableUI in the example above)) before calling repaint otherwise nothing will be updated.

Although the examples above don't exactly look great, it is possible to apply this effect and get a good looking result:


JXLayer is going to be part of JDK 7 so it's good to learn how to use it now!

No comments:

Post a Comment