Tuesday, August 08, 2006

Mighty Mitochondria! Them Cells Are Animated!

Java provides effortless loading and display of animated GIFs. Just tell ImageIcon where it can find the file, then plug the ImageIcon into a JLabel or JButton. Couldn't be simpler.




Now how about putting that animated icon into a JTree as feedback for loading status.





Oops. What happened? Did we get the right icon? Why isn't it animated? Try selecting one of the tree nodes, then rapidly move the selection up and down. Watch what happens.

What happened is that no one told the JTree it needed to repaint itself after it painted the initial image. Selection changes trigger repaint messages, which is why the icons move a little bit when you move the selection around. Any animation is driven by regular repaints of the component where it is drawn. JLabels and JButtons are no exception. Take a look at the source for ImageIcon.paintIcon:

public synchronized void paintIcon(Component c, Graphics g, int x, int y) {
if(imageObserver == null) {
g.drawImage(image, x, y, c);
} else {
g.drawImage(image, x, y, imageObserver);
}
}

When you create your ImageIcon, it doesn't have an image observer, so the component where it's being drawn is used instead (note that java.awt.Component implements ImageObserver):

public boolean imageUpdate(Image img, int infoflags,
int x, int y, int w, int h) {
int rate = -1;
if ((infoflags & (FRAMEBITS|ALLBITS)) != 0) {
rate = 0;
}
// ...
if (rate >= 0) {
repaint(rate, 0, 0, width, height);
}
return (infoflags & (ALLBITS|ABORT)) == 0;
}

So whenever the image reports that it has another frame ready (infoflags&FRAMEBITS != 0), it calls this method.

That's just fine for a label or button which only displays a single icon, but what about a tree, which might display this icon in every row? We need an image observer which can call repaint with the cell bounds of every cell that contains the animated icon. Using the tree itself as the observer could work, but we'd end up redrawing the entire tree on every frame. It's preferable to use a decorator which keeps track of which individual cells need updating.



In this case, let's use the tree cell renderer as the entry point or trigger to start or stop animation, since that's normally where a tree's icons get customized.


JTree tree = ...;
TreeCellRenderer r = new DefaultTreeCellRenderer() {
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded,
boolean leaf, int row, boolean focused) {
Component c =
super.getTreeCellRendererComponent(tree, value, selected, expanded,
leaf, row, focused);
TreePath path = tree.getPathForRow(row);
if (isLoading(path.getLastPathComponent()))
setIcon(LOADING_ICON);
// The following line is what you'd normally use; the rest is just illustrative
//CellAnimator.animate(tree, c, path);
// Let the CellAnimator utility figure out if there's an animated icon here
ImageIcon icon = (ImageIcon)getIcon();
if (CellAnimator.isAnimated(icon)) {
CellAnimator.animate(tree, new CellAnimator.TreeUpdater(tree, row), icon);
}
else {
CellAnimator.stop(tree, row);
}
return c;
}
};
tree.setCellRenderer(r);


The CellAnimator needs to do several things. At any point if it is determined that no animation is required, the animation support is disposed for the current location.

  1. Obtain the icon in use, if any
  2. If the icon is animated, install an ImageObserver
  3. Identify the location to receive repaint notifications, and add it to the observer's list


Whenever the animated icon posts an update, the ImageObserver for that icon will walk its list of repaint locations and trigger repaints. The actual implementation takes care of a number of other details, like using weak references for UI components and remove locations if the location or component is no longer valid.

The CellAnimator provides one-line methods to enable animation on standard Swing components and is easily extensible to enable animation on custom components that use the cell renderer technique.

Here is a demo of the cell animator applied to a lazy-loading tree.


The source includes JUnit-based unit tests which use the Abbot library.

5 comments:

jhmeyer said...

Can it be transferred to a single gif file? I'm sure the gif sharing community would really like to see some of the animations that can be produced. Great work!

Amanita said...

Hi, sorry to bother you, but it really caught my attention where you said:
"Java provides effortless loading and display of animated GIFs. Just tell ImageIcon where it can find the file, then plug the ImageIcon into a JLabel or JButton. Couldn't be simpler."
I'm trying to display an animated gif within a label, but it's not animating. I'm using:

image = ImageIO.read(new File(myImages.get(i)));
...
JLabel label = new JLabel(new ImageIcon(image));
panel.add(label);

(myImages is an array of file names)
I'm afraid your example is a bit beyond my level currently - is there a tutorial or example anywhere for updating the animation in a situation like mine?

technomage said...

You can use the animated icon class provided here, or almost as simply just instantiate a javax.swing.Timer which sets the JLabel icon to the next image in the series each time it fires.

The code would be simpler, though, if you just convert your series of files into a single animated GIF, then load that file into the JLabel.

Paulo Willers said...

Hi Tecnomage, I've tried to download the example code but the link is not working. Could you try to make it available again or send it to me?

technomage said...

All code is maintained at http://furbelow.sf.net.