Realtime Swing reflections (iTunes ain’t the only kid on the block)!
May 25th, 2007 |
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.
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);
}
...
}









“There’s no event that fires to report when repaints occur (I suppose Sun engineers figured it was just too likely to be abused)”
Actually there is. You can use the RepaintManager to catch repaints and update the reflection. And it works actually better than the approach you highlight here.
June 6th, 2007 at 5:29 pm
Posting code publicly appears to be a great way to prove that there’s always someone who knows more than you out there.
Mind you, I still can’t find any /eventing/, as your first sentence implies exists. I can see how installing a custom RepaintManager is better — it should halve the repaint cycles required and doesn’t require transparent components. Still, it looks about as much fun as writing my own KeyboardFocusManager. Which is to say not at all. I think until I hit a performance or transparency issue, I’ll stick with my bird in the hand.
June 8th, 2007 at 10:33 am