- Quest for Translucency
- Inside the Animated Cursor Library, Part 3
- Conclusion
Inside the Animated Cursor Library, Part 3
By utilizing a cursor image’s translucency values, the final animated cursor library implementation is able to present images such as the image shown in Figure 3. This implementation consists of the same packages and, with one exception, the same files that you discovered in Part 2. The exception is AniCursor.java, whose source code is presented in Listing 1.
Listing 1 AniCursor.java
// AniCursor.java // Create an animated cursor based on a Microsoft .ani file. package ca.mb.javajeff.anicursor; import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.io.*; import java.util.*; import javax.swing.*; import ca.mb.javajeff.cur.*; import ca.mb.javajeff.ico.*; import ca.mb.javajeff.riff.*; public class AniCursor implements Runnable { private volatile boolean finished; // Animation completion flag private Component comp; private Cursor [] cursors; private volatile int [] durations; private int [] sequence; private volatile int index; private ArrayList<BufferedImage> bilist; private boolean isTranslucent; // true if image has an alpha channel private GlassPane gp; private int cx, cy; // Current mouse position in component relative to the // glass pane private int hotspotx, hotspoty; private MouseListener ml; private MouseMotionListener mml; public AniCursor (String aniName, Component comp) throws BadAniException, IOException { this.comp = comp; if (aniName == null) { cursors = new Cursor [8]; cursors [0] = Cursor.getPredefinedCursor (Cursor.N_RESIZE_CURSOR); cursors [1] = Cursor.getPredefinedCursor (Cursor.NE_RESIZE_CURSOR); cursors [2] = Cursor.getPredefinedCursor (Cursor.E_RESIZE_CURSOR); cursors [3] = Cursor.getPredefinedCursor (Cursor.SE_RESIZE_CURSOR); cursors [4] = Cursor.getPredefinedCursor (Cursor.S_RESIZE_CURSOR); cursors [5] = Cursor.getPredefinedCursor (Cursor.SW_RESIZE_CURSOR); cursors [6] = Cursor.getPredefinedCursor (Cursor.W_RESIZE_CURSOR); cursors [7] = Cursor.getPredefinedCursor (Cursor.NW_RESIZE_CURSOR); durations = new int [8]; for (int i = 0; i < durations.length; i++) durations [i] = 75; sequence = new int [8]; for (int i = 0; i < sequence.length; i++) sequence [i] = i; } else { RIFF riff = null; try { riff = new RIFF (aniName); if (!riff.getFormType ().equals ("ACON")) throw new BadAniException ("not an animated cursor file"); bilist = new ArrayList<BufferedImage> (); int x = 0; int y = 0; Chunk chunk; while ((chunk = riff.getChunk ()) != null) if (chunk.name.equals ("anih")) { int size = ubyte (chunk.data [3]); size <<= 8; size |= ubyte (chunk.data [2]); size <<= 8; size |= ubyte (chunk.data [1]); size <<= 8; size |= ubyte (chunk.data [0]); int nframes = ubyte (chunk.data [7]); nframes <<= 8; nframes |= ubyte (chunk.data [6]); nframes <<= 8; nframes |= ubyte (chunk.data [5]); nframes <<= 8; nframes |= ubyte (chunk.data [4]); int nsteps = ubyte (chunk.data [11]); nsteps <<= 8; nsteps |= ubyte (chunk.data [10]); nsteps <<= 8; nsteps |= ubyte (chunk.data [9]); nsteps <<= 8; nsteps |= ubyte (chunk.data [8]); int width = ubyte (chunk.data [15]); width <<= 8; width |= ubyte (chunk.data [14]); width <<= 8; width |= ubyte (chunk.data [13]); width <<= 8; width |= ubyte (chunk.data [12]); int height = ubyte (chunk.data [19]); height <<= 8; height |= ubyte (chunk.data [18]); height <<= 8; height |= ubyte (chunk.data [17]); height <<= 8; height |= ubyte (chunk.data [16]); int bitcount = ubyte (chunk.data [23]); bitcount <<= 8; bitcount |= ubyte (chunk.data [22]); bitcount <<= 8; bitcount |= ubyte (chunk.data [21]); bitcount <<= 8; bitcount |= ubyte (chunk.data [20]); int nplanes = ubyte (chunk.data [27]); nplanes <<= 8; nplanes |= ubyte (chunk.data [26]); nplanes <<= 8; nplanes |= ubyte (chunk.data [25]); nplanes <<= 8; nplanes |= ubyte (chunk.data [24]); int disprate = ubyte (chunk.data [31]); disprate <<= 8; disprate |= ubyte (chunk.data [30]); disprate <<= 8; disprate |= ubyte (chunk.data [29]); disprate <<= 8; disprate |= ubyte (chunk.data [28]); int attr = ubyte (chunk.data [35]); attr <<= 8; attr |= ubyte (chunk.data [34]); attr <<= 8; attr |= ubyte (chunk.data [33]); attr <<= 8; attr |= ubyte (chunk.data [32]); if ((attr & 1) == 0) throw new BadAniException ("cannot deal with raw "+ "bitmap data"); durations = new int [nsteps]; for (int i = 0; i < durations.length; i++) durations [i] = disprate*1000/60; // convert from // jiffies to // milliseconds sequence = new int [nsteps]; for (int i = 0; i < sequence.length; i++) sequence [i] = i; } else if (chunk.name.equals ("icon")) { BufferedImage bi; int ncolors; if (chunk.data [2] == 1 && chunk.data [3] == 0) { Ico i; try { i = new Ico (new ByteArrayInputStream (chunk.data)); } catch (BadIcoResException bire) { throw new BadAniException (bire); } bi = i.getImage (0); ncolors = i.getNumColors (0); x = 0; y = 0; } else if (chunk.data [2] == 2 && chunk.data [3] == 0) { Cur c; try { c = new Cur (new ByteArrayInputStream (chunk.data)); } catch (BadCurResException bcre) { throw new BadAniException (bcre); } bi = c.getImage (0); ncolors = c.getNumColors (0); x = c.getHotspotX (0); y = c.getHotspotY (0); } else throw new BadAniException ("icon or cursor resource "+ "expected"); if (ncolors == 0 && !isTranslucent) { isTranslucent = true; hotspotx = x; hotspoty = y; Window w = SwingUtilities.getWindowAncestor (comp); if (w != null && (w instanceof JFrame)) { gp = new GlassPane ((JFrame) w); ((JFrame) w).setGlassPane (gp); } } bilist.add (bi); } else if (chunk.name.equals ("rate")) { if (durations == null) throw new BadAniException ("ANI header missing"); for (int i = 0; i < durations.length; i++) { int rate = ubyte (chunk.data [4*i+3]); rate <<= 8; rate |= ubyte (chunk.data [4*i+2]); rate <<= 8; rate |= ubyte (chunk.data [4*i+1]); rate <<= 8; rate |= ubyte (chunk.data [4*i]); durations [i] = rate*1000/60; // convert from jiffies // to milliseconds } } else if (chunk.name.equals ("seq ")) { if (sequence == null) throw new BadAniException ("ANI header missing"); for (int i = 0; i < sequence.length; i++) { int index = ubyte (chunk.data [4*i+3]); index <<= 8; index |= ubyte (chunk.data [4*i+2]); index <<= 8; index |= ubyte (chunk.data [4*i+1]); index <<= 8; index |= ubyte (chunk.data [4*i]); sequence [i] = index; } } cursors = new Cursor [bilist.size ()]; Toolkit toolkit = Toolkit.getDefaultToolkit (); for (int i = 0; i < bilist.size (); i++) cursors [i] = toolkit. createCustomCursor (bilist.get (i), new Point (x, y), "anicursor"); if (gp != null) { BufferedImage empty; empty = new BufferedImage (32, 32, BufferedImage.TYPE_INT_ARGB); gp.setCursor (toolkit.createCustomCursor (empty, new Point (0, 0), "empty")); } } catch (BadRIFFException briffe) { throw new BadAniException (briffe); } finally { if (riff != null) riff.close (); } if (durations == null) throw new BadAniException ("ANI header missing"); } } public void run () { index = 0; Runnable r = new Runnable () { public void run () { comp.setCursor (cursors [sequence [index]]); if (gp != null) gp.render (bilist.get (index), cx, cy); } }; while (!finished) { try { Thread.currentThread ().sleep (durations [index]); SwingUtilities.invokeAndWait (r); if (++index == durations.length) index = 0; } catch (Exception ex) { } } try { r = new Runnable () { public void run () { comp.setCursor (Cursor. getPredefinedCursor (Cursor.DEFAULT_CURSOR)); } }; SwingUtilities.invokeAndWait (r); } catch (Exception ex) { } } public void start () { if (isTranslucent) { ml = new MouseAdapter () { public void mouseEntered (MouseEvent me) { if (gp != null) gp.setVisible (true); } public void mouseExited (MouseEvent me) { if (gp != null) gp.setVisible (false); } }; comp.addMouseListener (ml); mml = new MouseMotionAdapter () { public void mouseMoved (MouseEvent me) { if (gp != null) { Point pt; pt = SwingUtilities.convertPoint (comp, me.getX (), me.getY (), gp); cx = pt.x-hotspotx; cy = pt.y-hotspoty; } } }; comp.addMouseMotionListener (mml); } finished = false; new Thread (this).start (); } public void stop () { finished = true; if (isTranslucent) { comp.removeMouseListener (ml); comp.removeMouseMotionListener (mml); } } private int ubyte (byte b) { return (b < 0) ? 256+b : b; // Convert byte to unsigned byte. } private class GlassPane extends JComponent { private JFrame frame; private BufferedImage bi; private int x, y; GlassPane (JFrame frame) { this.frame = frame; } public void paint (Graphics g) { if (bi != null) g.drawImage (bi, x, y, null); } void render (BufferedImage bi, int x, int y) { this.bi = bi; this.x = x; this.y = y; repaint (); } } }
In many ways, Listing 1 is identical to the AniCursor.java listings shown in the first two parts of this series. Because I’ve already discussed the architectures presented in those listings, I won’t revisit them here—you might want to refer back to Part 1 and Part 2 to refresh your memory. Instead, I’ll focus on how Listing 1 makes cursor images look better.
Listing 1 achieves its objective of making cursor images look better by utilizing a component that is created from the private GlassPane class. This class’s paint() method renders an image on a frame window’s glass pane via the Graphics class’s drawImage() method. When asked to render the contents of a BufferedImage with an alpha channel, this method honors translucency.
The GlassPane component is created by AniCursor’s constructor when the first image containing an alpha channel is extracted from the .ani file. This component is then assigned to the frame window of the component over which the cursor is animated. This window is obtained with the help of the SwingUtilities.getWindowAncestor() method.
Assigning the glass pane to the frame window is not enough: The glass pane must also be made visible whenever the mouse appears over the component, and hidden when the mouse exits the component. These tasks are taken care of by a mouse listener that’s registered with the component in AniCursor’s start() method, and deregistered in AniCursor’s stop() method.
The start() method also registers a mouse motion listener whose job is to update a couple of field variables—cx and cy—with the upper-left corner of the cursor image as the mouse travels over the component. Because the image will be drawn on the glass pane, these coordinates must be relative to the glass pane’s upper-left corner origin.
The mouse motion listener doesn’t ask the glass pane to render the cursor image because this task is performed by AniCursor’s run() method. After all, the run() method already invokes setCursor() to assign the next cursor image to the component—these images aren’t shown when the glass pane is visible.
Volatility Revisited
Whenever multiple threads access the same variables, problems related to the lack of synchronization can arise. Fortunately, AniCursor’s architecture is such that synchronization-related problems don’t occur when its "animation" thread competes with the event-dispatching thread to read values from/write values to the variables shared between these threads.
But another problem potentially exists. This problem arises from each thread keeping its own copy of a variable (for performance reasons). Although I have not (yet) encountered this situation on machines with single processors with single cores, I believe that it’s common for threads to keep their own copies on machines with multiple processors, and even machines whose processors contain multiple cores.
When one thread writes a value to its variable copy, another thread will not see this updated value until the first copy’s value is flushed to and the second copy’s value is refreshed from main memory. For example, consider AniCursor. Assuming two copies of the finished variable, calling the stop() method on the event-dispatching thread might not affect the other thread’s copy of this variable, and run() won’t end.
If you recall Part 1, I used the volatile keyword to indicate that threads should not have their own copies of the finished, comp, and cursors variables. I marked finished volatile because the "animation" and event-dispatching threads access this variable. Unfortunately, I neglected to do the same for index, which is also accessed by these threads.
When I created the first AniCursor implementation, I considered allowing AniCursor’s constructor to be called on a thread other than the event-dispatching thread—start() and stop() should be called on the event-dispatching thread. This is the reason for marking comp and cursors volatile.
I chose to ignore the volatility issue in Part 2’s implementation because the first two implementations are throwaways. However, because this article’s implementation is the preferred implementation, I’ve addressed this issue by marking only the finished, durations, and index variables volatile—they’re the only variables accessed by the "animation" and event-dispatching threads.