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. JLabel
s and JButton
s 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.- Obtain the icon in use, if any
- If the icon is animated, install an
ImageObserver
- 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.