- Let There Be Animated Cursors
- Inside the Animated Cursor Library
- Conclusion
Inside the Animated Cursor Library
The animated cursor library’s basic implementation code is organized into a main package that depends on two support packages. The main package is named ca.mb.javajeff.anicursor and consists of two class members: AniCursor and BadAniException. Listing 2 presents AniCursor’s source code.
Listing 2 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.image.*; import java.io.*; import java.util.*; import javax.swing.*; import ca.mb.javajeff.cur.*; import ca.mb.javajeff.riff.*; public class AniCursor implements Runnable { private volatile boolean finished; // Animation completion flag private volatile Component comp; private volatile Cursor [] cursors; private int index; 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); } else { RIFF riff = null; try { riff = new RIFF (aniName); if (!riff.getFormType ().equals ("ACON")) throw new BadAniException ("not an animated cursor file"); ArrayList<BufferedImage> bilist; bilist = new ArrayList<BufferedImage> (); int x = 0; int y = 0; Chunk chunk; while ((chunk = riff.getChunk ()) != null) if (chunk.name.equals ("icon")) { Cur c = new Cur (new ByteArrayInputStream (chunk.data)); x = c.getHotspotX (0); y = c.getHotspotY (0); BufferedImage bi = c.getImage (0); // Convert a translucent alpha channel, where alpha // ranges from 0 to 255, to a binary alpha channel, where // alpha is either 0 or 255. if (c.getNumColors (0) == 0) for (int i = 0; i < bi.getHeight (); i++) { int [] rgb = bi.getRGB (0, i, bi.getWidth (), 1, null, 0, bi.getWidth ()*4); for (int j = 0; j < rgb.length; j++) { int alpha = (rgb [j] >> 24) & 255; if (alpha < 0x80) alpha = 0; else alpha = 255; rgb [j] &= 0x00ffffff; rgb [j] = (alpha << 24) | rgb [j]; } bi.setRGB (0, i, bi.getWidth (), 1, rgb, 0, bi.getWidth ()*4); } bilist.add (bi); 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"); } } catch (BadCurResException bcre) { throw new BadAniException (bcre); } catch (BadRIFFException briffe) { throw new BadAniException (briffe); } finally { if (riff != null) riff.close (); } } } public void run () { index = 0; Runnable r = new Runnable () { public void run () { comp.setCursor (cursors [index%cursors.length]); } }; while (!finished) { try { Thread.currentThread ().sleep (75); SwingUtilities.invokeAndWait (r); index++; } 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 () { finished = false; new Thread (this).start (); } public void stop () { finished = true; } }
The public AniCursor(String aniName, Component comp) constructor initializes the library. After saving the comp reference for use by the animation thread, the constructor builds an array of Cursor objects. Each object describes one cursor image from the aniName-identified .ani file, or is one of Java’s predefined cursors if aniName is null.
Assuming that aniName is not null, the constructor extracts the entire animation sequence of cursor images from the .ani file. It accomplishes this task with the help of the RIFF and Cur classes that are located in the ca.mb.javajeff.riff and ca.mb.javajeff.cur support packages, respectively.
The constructor uses the RIFF class to navigate through the Resource Interchange File Format (RIFF) based .ani file, extracting icon-named chunks of data from this file—these chunks store cursor images. I’ll have more to say about the RIFF format and same-named class in the second part of this series.
After extracting an icon-named chunk, AniCursor’s constructor passes the chunk data to one of the Cur class’ constructors to read and parse this data. Cur’s constructor assumes that the data adheres to Microsoft’s Cursor Resource Format, meaning that this cursor resource stores one or more cursor images along with associated hotspot information (for use in hit testing).
Moving on, AniCursor’s constructor uses Cur to retrieve the first cursor image and its hotspot from the cursor resource, removes any translucency information from the image (because cursor images with translucent alpha channels look bad in a cursor context), and converts the resulting image and its hotspot into a Cursor object, which it then stores in the Cursor array.
If an IOException is thrown from a RIFF or Cur method, the constructor forwards this exception to its caller. The constructor also packages any thrown RIFF-specific or Cur-specific exception into a BadAniException object (to provide a simplified public API), which it then throws to its caller. BadAniException is presented in Listing 3.
Listing 3 BadAniException.java.
// BadAniException.java package ca.mb.javajeff.anicursor; public class BadAniException extends Exception { public BadAniException (String message) { super (message); } public BadAniException (Throwable throwable) { super (throwable); } }
In addition to a constructor, AniCursor’s public API consists of public void start() and public void stop() methods, which are used to start and stop the animation thread. Each time start() is called, a new animation thread is created and starts running. This thread stops, possibly after a brief delay if the thread is sleeping or waiting, when stop() is called.
The animation thread runs the public void run() method’s code. This code repeatedly sleeps for 75 milliseconds, and then invokes the saved component’s setCursor() method to establish the next cursor in the component’s cursor animation sequence. This cursor will be seen only if the mouse is hovering over the component. Before exiting, run() assigns the default cursor to the component.
To avoid synchronization trouble, the animation thread calls setCursor() on the event-dispatching thread and waits for setCursor() to finish. Also, variables accessed from the event-dispatching and animation threads (such as finished) are marked volatile. This keyword prevents each thread from maintaining its own copy of the variable—separate variable copies are problematic.