Blogs / Tech Blog

Realtime Swing reflections (iTunes ain’t the only kid on the block)!

Achtung Baby reflected

Check out this reflection magic! Now iTunes isn’t the only one with fancy reflections on album art. The best part about it is that it’s a general use component that doesn’t require customization each time. It can wrap any transparent JComponent and it will automatically repaints whenever the contained component changes. You see the text appearing in the reflection as you type in the text field. Try the Web Start ReflectionDemo. Source code is provided in reflectiondemo.jar, and an explanation of how it’s done follows.

This component is a combination of several techniques:

  • Image fading.
  • Applying a transform so the contained component is painted upside down.
  • Active update accomplished by, *gasp*, calling repaint during paintComponent!

Fading images

You can paint colors/lines/solid shapes/etc using a GradientPaint to create an object that fades from solid to transparent as we desired, but getting a complex graphic to do the same thing didn’t seem possible until we learned about AlphaComposite.DST_IN, (see our previous blog posting on the subject). It’s a class that, instead of blending two images together, allows you to use the color bytes from one image and the alpha from the second image. This technique allowed me to composite together a fully opaque picture of a component with a rectangle of black painted using a standard Gradient paint.

Imagine the icon upside down and you’ll see what I’m trying to accomplish here.

SourceAlphaComposite

Inverting the component

The reflection is obtained by applying transforms to a Graphics object, then permitting the reflecting component to paint itself to a BufferedImage instead of the supplied Graphics instance. This results in a copy of the contained component painted upside down in a separate BufferedImage. After compositing using the above technique, you get something that looks like the component but upside down and faded with whatever GradientPaint you used.

You can paint upside down to the supplied Graphics, of course. However, if you want the fading effect to apply only to the contained component, you need to isolate it on a separate BufferedImage until it’s fully prepared.

In the below snippet of code, reflG2D is the Graphics2D of the offscreen buffer that holds the reflection so that it may be faded before painting to screen. The scaling step causes all y-coordinates to be multiplied by -1, so when you paint the children of ReflectionComponent to reflG2D, source paints itself upside down.

Look out, though, that means the paint which would normally be (0,0) -> (source.getWidth(), source.getHeight()) is now (0,0) -> (source.getWidth(), -source.getHeight()). You've just painted outside the boundaries of the image so you translate down to put it in the visible region. The rest of the translation ensures proper painting even if source isn't at (0,0) due to insets or the layout manager.

	Insets i = getInsets();
	reflG2D.scale(1,-1);
	reflG2D.translate(-source.getX(), - (source.getHeight()+i.top));

	super.paintChildren(reflG2D);

	reflG2D.translate(source.getX(), (source.getHeight()+i.top));
	reflG2D.scale(1,-1);

Don't forget to undo the changes you made so the fade operations can be done without being affected by the inversion/translation.

Active update of the reflection

This is a bit sketchy but too cool to pass up. I've seen several reflection component examples out there, but they don't seem to do active update, so here it is. There's no event that fires to report when repaints occur (I suppose Sun engineers figured it was just too likely to be abused). They're probably right. So I abused opacity instead. When a component is marked for repaint and it is transparent (i.e. isOpaque() == false), then the parent of said component will be repainted as well. So, if everything contained in a ReflectionComponent is transparent, then ReflectionComponent itself will repaint every time the contained object does so. The only hurdle to overcome is the crop -- the dirty region will never automatically include the reflection region because the reflection lies outside the bounds of the contained component. This makes it necessary to call repaint on the reflection region as well. Basically it doubles up on every repaint, not something you REALLY want to do all over the place but it does the job.

Here's how I did it:

public void paintComponent(Graphics g) {
	...
	Rectangle r = g.getClipBounds();
	if( (r.y+r.height) < (getHeight() - 1) ) {
		repaint(r.x, r.y, r.width, getHeight() - r.y);
	}
	...
}
Other Blogs