Animate Your JList Selections





Animate Your JList Selections

Fading in and catching the eye.

Not every GUI involves windows and mouse pointers, and the visual language of a GUI can be very different depending on what is provided by the environment. Typically, GUIs for things like console video games and settop boxes don't use a mouse metaphor, so there's no onscreen pointer that the user is tracking. As a result, these systems often give the user more profound feedback when they move around a listhighlights slide from one item to another, selected items fade in while deselected items fade out, etc. so there's something the eye can track. You can do the same thing in Swing, with more cell-rendering hackery. You might not need it now, but it'll be handy if you ever design a kiosk with Swing.

One way to show a changed selection is to show a brief animation of the cell selection. Instead of just being highlighted instantly, you fade the selected cell from its unselected background and foreground colors to its selected colors over the course of a short time (really short, like a half-second, so it isn't annoying).

To do this, you'll need to create an animator thread that kicks off every time the selection changes. This short-lived thread repeatedly updates a highlight color and calls repaint( ). The cell renderer can then use the updated highlight color as it redraws the cells in the list. Figure shows this technique.

Animating the JList cell selection
import java.awt.*; 
import javax.swing.*; 
import javax.swing.event.*; 
import java.util.*;
public class AnimatedJList extends JList 
    implements ListSelectionListener {

    static java.util.Random rand = new java.util.Random();


    static Color listForeground, listBackground, 
        listSelectionForeground, listSelectionBackground; 
    static float[] foregroundComps, backgroundComps, 
        foregroundSelectionComps, backgroundSelectionComps;

    static {        
        UIDefaults uid = UIManager.getLookAndFeel().getDefaults(); 
        listForeground = uid.getColor ("List.foreground"); 
        listBackground = uid.getColor ("List.background");
        listSelectionForeground = uid.getColor ("List.selectionForeground");
        listSelectionBackground = uid.getColor ("List.selectionBackground");
        foregroundComps =
            listForeground.getRGBColorComponents(null);
        foregroundSelectionComps =
            listSelectionForeground.getRGBColorComponents(null);
backgroundComps =
            listBackground.getRGBColorComponents(null);
backgroundSelectionComps =
            listSelectionBackground.getRGBColorComponents(null); 
    } 
    public Color colorizedSelectionForeground,
        colorizedSelectionBackground;

    public static final int ANIMATION_DURATION = 1000; 
    public static final int ANIMATION_REFRESH = 50;
  
    public AnimatedJList() {        
        super(); 
        addListSelectionListener (this);        
        setCellRenderer (new AnimatedCellRenderer());
    }

    public void valueChanged (ListSelectionEvent lse) {
        if (! lse.getValueIsAdjusting()) {
            HashSet selections = new HashSet();
            for (int i=0; i < getModel().getSize(); i++) {
                if (getSelectionModel().isSelectedIndex(i))
                    selections.add (new Integer(i)); 
            }            
            CellAnimator animator = new CellAnimator (selections.toArray());            
            animator.start();
        } 
    }
public static void main (String[] args) {
        JList list = new AnimatedJList ();
        DefaultListModel defModel = new DefaultListModel();
        list.setModel (defModel);
        String[] listItems = {
            "Chris", "Joshua", "Daniel", "Michael",
            "Don", "Kimi", "Kelly", "Keagan"
    };
    Iterator it = Arrays.asList(listItems).iterator();
    while (it.hasNext())
        defModel.addElement (it.next());
    // show list
    JScrollPane scroller =
        new JScrollPane (list, 
                         ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, 
                         ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
    JFrame frame = new JFrame ("Checkbox JList");
    frame.getContentPane().add (scroller);
    frame.pack();
    frame.setVisible(true);
     }

 class CellAnimator extends Thread {
     Object[] selections;
     long startTime;
     long stopTime;
     public CellAnimator (Object[] s) {
         selections = s;
     }
     public void run() {
         startTime = System.currentTimeMillis(); 
         stopTime = startTime + ANIMATION_DURATION;            
         while (System.currentTimeMillis() < stopTime) {
              colorizeSelections();
              repaint();
              try { Thread.sleep (ANIMATION_REFRESH); }
              catch (InterruptedException ie) {}
         }
         // one more, at 100% selected color
         colorizeSelections();
         repaint();
     }

   // colorizeSelections() listing below

      // AnimatedCellRenderer listing below 
}

Like several previous hacks, this hack starts with some static code to get the platform colors for selected and unselected foreground and background colors. But this time, it also saves them off into arrays of their red, green, and blue components.

The component sets up a ListSelectionListener, which fires off a CellAnimator every time it gets the last of a series of ListSelectionEvents. The CellAnimator is a thread that runs for a short time only (defined by the class variable ANIMATION_DURATION), repeatedly calling colorizeSelections( ) and then sleeping briefly.

colorizeSelections( ), shown in Figure, calculates a float to express how much of the animation duration has elapsed. It then applies this as a proportion to the distance between the start and end values for each of the red, green, and blue components. For example, if the unselected background color is white (255, 255, 255), and the selected color is pure blue (0, 0, 255), then halfway through the animation the color should be (127, 127, 255), where 127 is halfway between the start and end values of the red and green components, and blue doesn't change.

Determining animation color for selected cells
public void colorizeSelections() { 
    // calculate % completion relative to start/stop times    
    float elapsed = (float) (System.currentTimeMillis() - startTime);
    float completeness = Math.min ((elapsed/ANIMATION_DURATION), 1.0f); 
    // calculate scaled color 
    float colorizedForeComps[] = new float[3]; 
    float colorizedBackComps[] = new float[3]; 
    for (int i=0; i<3; i++) {
        colorizedForeComps[i] =
           foregroundComps[i] +
           (completeness *
            (foregroundSelectionComps[i] - foregroundComps[i]));
        colorizedBackComps[i] =
           backgroundComps[i] +
           (completeness *
            (backgroundSelectionComps[i] - backgroundComps[i]));
    }
    colorizedSelectionForeground =
       new Color (colorizedForeComps[0],
                  colorizedForeComps[1],
                  colorizedForeComps[2]);

    colorizedSelectionBackground =
       new Color (colorizedBackComps[0],
       colorizedBackComps[1],
       colorizedBackComps[2]);
}

The cell renderer in Figure is very simple: it just looks to see if the cell it's rendering is selected; if so, it sets its foreground and background to the colorized values. It also sets the cell to be opaque, meaning that the renderer wants the responsibility of drawing all the pixels, which is necessary to make the background color fill the cell. In a more complex cell layout, you might need to apply the foreground and background colors to all the cell's children and make them opaque, too.

Rendering the animated list cells
class AnimatedCellRenderer extends DefaultListCellRenderer {
    public Component getListCellRendererComponent(JList list,
                                                  Object value,
                                                  int index,
                                                  boolean isSelected,
                                                  boolean hasFocus) {
       Component returnMe = 
           super.getListCellRendererComponent (list, value, index, 
                                               isSelected, hasFocus);
       if (isSelected) { 
           returnMe.setForeground (colorizedSelectionForeground); 
           returnMe.setBackground (colorizedSelectionBackground); 
           /* this might be necessary if you have more
              elaborate cells
           if (returnMe instanceof Container) {
               Component[] children =
                   ((Container)returnMe).getComponents (); 
               System.out.println (children.length + " children"); 
               for (int i=0;
                    (children != null ) && (i<children.length); i++) {
                     children[i].setForeground (colorizedSelectionForeground); 
               children[i].setBackground (colorizedSelectionBackground); 
               }
           }
           */
           if (returnMe instanceof JComponent)
               ((JComponent) returnMe).setOpaque(true);
           }
           return returnMe;
         } 
}

When you run the code, clicking on a cell makes it briefly fade into the selection color, as seen in Figure.

One potential improvement: if your list allows multiple selections, then all the selected cells will animate, and it would make more sense to animate just the one that the user has clicked on. You could do this by figuring out (by caching previous selections [Hack #15]) which item is the new selection, and setting a flag so that the cell renderer applies only the colorized foreground and background colors to that cell.

Fading in a cell selection



     Python   SQL   Java   php   Perl 
     game development   web development   internet   *nix   graphics   hardware 
     telecommunications   C++ 
     Flash   Active Directory   Windows