When we showed off the REALiTY demo for Extreme GUI Makeover 2007 at JavaOne, I demonstrated translucent shaped windows in Java. The code behind this effect is a lot like other effects from the same demo.
In REALiTY, translucent shaped windows are used to show more details about the selected table row. You can see how it looks like in the following screenshot or in one of theses two videos: QuickTime high quality and QuickTime low quality.
I really encourage you to look at the videos to see how the shaped window is animated. When shown and disposed, a fade animation is displayed. Whenever the selection changes in the table, the window smoothly follows the selection. Also, the details window sticks to the main frame when the user moves it around.
First of all, the window’s background is drawn with a simple PNG image. I could have implemented it in Java 2D, with the same tricks used for the blurry dialog, but I was running out of time. Window’s translucency is achieved with the help of JNA. I previously described how to use JNA on this blog so I won’t waste your time here.
Everything else is implemented using the Beans Binding framework and the Timing Framework. Those two libraries made the code a breeze to write. For instance, the following snippet shows how to make the window fade:
public void hideWindow() {
Animator animator = PropertySetter.createAnimator(
400, this, "alternateAlpha", 0.0f);
animator.setAcceleration(0.2f);
animator.setDeceleration(0.3f);
animator.addTarget(new TimingTargetAdapter() {
@Override
public void end() {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
setVisible(false);
}
});
}
});
animator.start();
}
public float getAlternateAlpha() {
return alternateAlpha;
}
public void setAlternateAlpha(float alternateAlpha) {
this.alternateAlpha = alternateAlpha;
AlphaWindow.updateAlphaForWindow(this, alternateAlpha);
}
As you can see, we simply set up an animator and let the Timing Framework and JNA do their job. To make the window follow the selection is equally easy. In the following snippet, AlternateDetailPanel refers to the panel that contains the pictures shown in the translucent window. We simply bind the selectionBound property of the table to the animatedLocation location of the window.
void setAlternateDetailView(AlternateDetailPanel detailView) {
BindingContext ctx = new BindingContext();
Binding binding = new Binding(this, "${selectionBounds}",
SwingUtilities.getWindowAncestor(detailView), "animatedLocation");
binding.setConverter(BOUNDS_CONVERTER);
ctx.addBinding(binding);
ctx.addBinding(this, "${selectedHome.photoList}", detailView, "photoList");
ctx.bind();
}
The selectionBound is a custom property that defines the boundaries of the selected row in screen coordinates. Here are the getter and setter for that property:
public Rectangle getSelectionBounds() {
return selectionBounds;
}
private void setSelectionBounds(int row) {
Rectangle oldBounds = selectionBounds;
selectionBounds = table.getCellRect(row, 0, true);
Point screen = table.getLocationOnScreen();
selectionBounds.x += screen.x;
selectionBounds.y += screen.y;
selectionBounds.width = table.getWidth();
firePropertyChange("selectionBounds", oldBounds, selectionBounds);
}
The setSelectionBound() method is called internally every time the selection changes. This in turn triggers the bean binding setup earlier and invokes setAnimatedLocation() on the window. Once again, the Timing Framework does all of the work for us:
public Point getAnimatedLocation() {
return getLocation();
}
public void setAnimatedLocation(Point p) {
Animator animator = PropertySetter.createAnimator(
200, this, "location", p);
animator.setAcceleration(0.1f);
animator.setDeceleration(0.1f);
animator.start();
}
You might have noticed that the selectionBound is a Rectangle whereas the animatedLocation is a Point. How do we convert from one to the other? This is the purpose of the line binding.setConverter(BOUNDS_CONVERTER) used when setting up the binding. This converter takes a rectangle and turns it into a point. The point is defined as being vertically centered in the rectangle and shifted to the left of the right edge:
public static final BindingConverter BOUNDS_CONVERTER =
new BindingConverter() {
@Override
public Object sourceToTarget(Object value) {
Rectangle bounds = (Rectangle) value;
return new Point(bounds.x + bounds.width - 35,
bounds.y + bounds.height / 2);
}
@Override
public Object targetToSource(Object value) {
return null;
}
};
Last but not least we need to make the shaped window follow the main frame when it moves. Doing so is quite easy but requires two code paths, one for Windows, one for Mac OS X. On Windows, when you drag a frame, the componentMoved event is called a regular interval. However, on Mac OS X, this event is triggered only when you release the frame after the drag. That’s why on Windows we simply update the location of the shaped window when the main frame moves, whereas on Mac OS X we animated the shaped window to its new location:
theReality.getMainFrame().addComponentListener(new ComponentAdapter() {
@Override
public void componentMoved(ComponentEvent e) {
Point location = reality.getMainFrame().getLocation();
int dx = location.x - lastKnownLocation.x;
int dy = location.y - lastKnownLocation.y;
Point p = getLocation();
p.x += dx;
p.y += dy;
if (OperatingSystemSupport.isWindows()) {
setLocation(p);
} else {
setAnimatedLocation(p);
}
}
});
Even though this effect is not the most impressive visually, I find it very interesting because of the way it relies on the Beans Binding framework. This framework is meant to bind data between components&emdash;don’t forget that visual properties like dimensions and locations are also data!
Do not hesitate to read the session slides for further information.