Java tip: How to add zebra background stripes to a JList

Technologies: Java 5+

Zebra stripes are subtle alternating stripes painted behind list items in a graphical user interface (GUI). They improve the readability of wide and long lists, but the JList class in Java's Swing doesn't support them. This tip shows how to extend JList to add zebra background stripes.

Code

The Java ZebraJList class below extends JList in Java's Swing package. It does three things:

  • It overrides the paintComponent() method so that it can fill the list's background with zebra stripes before painting the list's components.
  • It wraps the list's cell renderer so that it can set the background color of opaque list components to match the stripes.
  • It chooses zebra stripe colors automatically based upon the list's current background and selected background colors.

The ZebraJList class has been tested and it works on all operating systems, with all Java look and feel choices, and with standard or application-specific list cell renderers. It automatically updates it's background stripe colors on changes to the component's colors made by the Java application, by the installed look and feel, or by the user when they change their OS-wide color theme (such as through the Windows Display control panel or the Mac or Linux Appearance preferences pane).

A few examples for ZebraJList and more explanations follow the Java code in the next sections.

A ZebraJList with light blue zebra stripes

/**
 * A JList that supports a zebra stripe background.
 */
public class ZebraJList
    extends javax.swing.JList
{
    private java.awt.Color rowColors[] = new java.awt.Color[2];
    private boolean drawStripes = false;
 
    public ZebraJList( )
    {
    }
    public ZebraJList( javax.swing.ListModel dataModel )
    {
        super( dataModel );
    }
    public ZebraJList( Object[] listData )
    {
        super( listData );
    }
    public ZebraJList( java.util.Vector<?> listData )
    {
        super( listData );
    }
 
    /** Add zebra stripes to the background. */
    public void paintComponent( java.awt.Graphics g )
    {
        drawStripes = (getLayoutOrientation( )==VERTICAL) && isOpaque( );
        if ( !drawStripes )
        {
            super.paintComponent( g );
            return;
        }
 
        // Paint zebra background stripes
        updateZebraColors( );
        final java.awt.Insets insets = getInsets( );
        final int w   = getWidth( )  - insets.left - insets.right;
        final int h   = getHeight( ) - insets.top  - insets.bottom;
        final int x   = insets.left;
        int y         = insets.top;
        int nRows     = 0;
        int startRow  = 0;
        int rowHeight = getFixedCellHeight( );
        if ( rowHeight > 0 )
            nRows = h / rowHeight;
        else
        {
            // Paint non-uniform height rows first
            final int nItems = getModel( ).getSize( );
            rowHeight = 17; // A default for empty lists
            for ( int i = 0; i < nItems; i++, y+=rowHeight )
            {
                rowHeight = getCellBounds( i, i ).height;
                g.setColor( rowColors[i&1] );
                g.fillRect( x, y, w, rowHeight );
            }
            // Use last row height for remainder of list area
            nRows    = nItems + (insets.top + h - y) / rowHeight;
            startRow = nItems;
        }
        for ( int i = startRow; i < nRows; i++, y+=rowHeight )
        {
            g.setColor( rowColors[i&1] );
            g.fillRect( x, y, w, rowHeight );
        }
        final int remainder = insets.top + h - y;
        if ( remainder > 0 )
        {
            g.setColor( rowColors[nRows&1] );
            g.fillRect( x, y, w, remainder );
        }
 
        // Paint component
        setOpaque( false );
        super.paintComponent( g );
        setOpaque( true );
    }
 
    /** Wrap a cell renderer to add zebra stripes behind list cells. */
    private class RendererWrapper
        implements javax.swing.ListCellRenderer
    {
        public javax.swing.ListCellRenderer ren = null;
 
        public java.awt.Component getListCellRendererComponent(
            javax.swing.JList list, Object value, int index,
            boolean isSelected, boolean cellHasFocus )
        {
            final java.awt.Component c = ren.getListCellRendererComponent(
                list, value, index, isSelected, cellHasFocus );
            if ( !isSelected && drawStripes )
                c.setBackground( rowColors[index&1] );
            return c;
        }
    }
    private RendererWrapper wrapper = null;
 
    /** Return the wrapped cell renderer. */
    public javax.swing.ListCellRenderer getCellRenderer( )
    {
        final javax.swing.ListCellRenderer ren = super.getCellRenderer( );
        if ( ren == null )
            return null;
        if ( wrapper == null )
            wrapper = new RendererWrapper( );
        wrapper.ren = ren;
        return wrapper;
    }
 
    /** Compute zebra background stripe colors. */
    private void updateZebraColors( )
    {
        if ( (rowColors[0] = getBackground( )) == null )
        {
            rowColors[0] = rowColors[1] = java.awt.Color.white;
            return;
        }
        final java.awt.Color sel = getSelectionBackground( );
        if ( sel == null )
        {
            rowColors[1] = rowColors[0];
            return;
        }
        final float[] bgHSB = java.awt.Color.RGBtoHSB(
            rowColors[0].getRed( ), rowColors[0].getGreen( ),
            rowColors[0].getBlue( ), null );
        final float[] selHSB  = java.awt.Color.RGBtoHSB(
            sel.getRed( ), sel.getGreen( ), sel.getBlue( ), null );
        rowColors[1] = java.awt.Color.getHSBColor(
            (selHSB[1]==0.0||selHSB[2]==0.0) ? bgHSB[0] : selHSB[0],
            0.1f * selHSB[1] + 0.9f * bgHSB[1],
            bgHSB[2] + ((bgHSB[2]<0.5f) ? 0.05f : -0.05f) );
    }
}

Examples

Construct a ZebraJList and add it to a scroll pane:

ZebraJList list = new ZebraJList( items );
JScrollPane scrollList = new JScrollPane( list );

Construct a ZebraJList, set the fixed row height (for faster drawing), and add the list to a scroll pane:

ZebraJList list = new ZebraJList( items );
list.setFixedCellHeight( 20 );
JScrollPane scrollList = new JScrollPane( list );

Construct a ZebraJList, set the prototype cell value (for faster drawing), and add the list to a scroll pane:

ZebraJList list = new ZebraJList( items );
list.setPrototypeCellValue( "typical item" );
JScrollPane scrollList = new JScrollPane( list );

Construct a ZebraJList, set the prototype cell value to the first item, set custom colors, and add the list to a scroll pane:

ZebraJList list = new ZebraJList( items );
list.setPrototypeCellValue( items[0] );
list.setBackground( Color.darkGray );
list.setForeground( Color.white );
list.setSelectionBackground( Color.yellow );
list.setSelectionForeground( Color.black );
JScrollPane scrollList = new JScrollPane( list );

Explanation

A normal Java JList object is painted in two steps:

  1. If the list is opaque, the list's background is painted with a solid color (often white or light gray).
  2. For each list item, a component is retrieved from the list's cell renderer and painted (often it's a JLabel to draw a list item's name).

To add zebra background stripes, both of these steps must be overridden.

For step 1, overriding the list's background painting lets ZebraJList pre-fill the entire background area with stripes before any list items are drawn on top. These stripes are visible when the list is empty, when it is too short to fill the JList's scroll pane viewport, or when the list's items are painted with non-opaque cell components.

For step 2, overriding the list cell renderer lets ZebraJList set the background colors for opaque cell components rendered atop the background (such as a JLabel or JCheckBox).

If you don't override both steps, you'll only get partial results that will look odd. For instance, if you override step 1, but not step 2, your list will have stripes everywhere except under opaque list items. And if you override step 2, but not step 1, your list will have stripes under list items, but not everywhere else.

Painting background stripes on the JList

Normally, Java's JList paintComponent() method calls the list's UI delegate to draw the background and the list's components. ZebraJList overrides paintComponent() to get in there first and paint alternating background stripes the width of the list and the height of each row. It then temporarily turns off opacity and call's the JList's normal paintComponent() method to let the UI delegate paint the list cells. With opacity disabled, the UI delegate won't repaint the JList background, leaving the zebra stripes alone.

For flexibility, JList and ZebraJList both handle lists with unknown or varying row heights (imagine a list of paragraphs of different sizes, for instance). For better performance on uniform row height lists (like most lists), Java applications should call setFixedCellHeight() to set a row height, or call setPrototypeCellValue() to provide a typical list item from which to calculate a fixed row height. In fact, this is a good thing to do on all JLists, not just those with zebra stripes.

Non-opaque components shouldn't fill their backgrounds, so ZebraJList skips the background stripes if the list is not opaque (however, opaque lists are the default in Java and most applications leave it that way).

JList also supports several list styles. The default is a standard vertical list, but JList also has "newspaper style" layouts that wrap long lists into multiple side-by-side columns. Zebra stripes for these lists look odd. They also raise usability concerns because the shared background color of side-by-side items could imply a connection between them that may not be true. So, ZebraJList skips background stripes if the list's style isn't vertical.

Painting background stripes on list items

Normally, Java's JList getCellRenderer() method returns a ListCellRenderer that returns a component to render each list item. The default renderer is a DefaultListCellRenderer that usually returns a JLabel for each item. Applications can create their own renderers to create lists of text fields, lists of panels, or whatever.

A renderer's component paints its own background, if it is opaque, and then the item's value. ZebraJList overrides getCellRenderer() and inserts a wrapper ListCellRenderer that calls the original renderer (the default one or the application's own renderer) and sets the background color of returned components. When the components are painted, their backgrounds will match the list's zebra stripes.

Setting zebra background stripe colors

Certainly, you can use any colors you like for the zebra stripes. But it would be nice if the colors matched the user's color choices for their OS, or the application's overall color choices, or the installed look and feel's color choices.

Unfortunately, Java does not provide SystemColor objects or look and feel properties for zebra stripe colors. So, ZebraJList makes a guess. It uses the JList's background color for even stripes, and computes a subtly different background color for odd stripes. Both of these colors are updated on each repaint so that the list's colors automatically change whenever the application, the look and feel, or the user changes their color theme.

You can test the color guesses by showing a ZebraJList then changing your OS color theme. Use the Display control panel in Windows, and the Appearance preference pane on the Mac or Linux.

Testing

This Java class has been tested and it works well on the CDE/Motif, GTK+, Mac OS X, Metal, Windows, and Windows Classic look and feel choices on Linux, Mac, and Windows platforms for Java 5 and Java 6. The automatic zebra stripe color algorithm works pretty well for all standard color schemes on these platforms, but you may wish to tune it for your own application's needs.

Further reading

Related tips

Other articles and specifications

Comments

It's Very Good

It's Very Good

Nice, I think I'll be using

Nice, I think I'll be using this from now. Great.

Another approache

While putting together my own solution, I found that the UI delegate was the culprit for filling the background (specifically the ui.update(...) method).

Rather then calling setOpaque(...) which may result in calls be made to any number of listeners and causing the rendering process to slow, you could use ui.paint(...) method instead.

Of course, you are still responsible for honoring the opaque state of the component, but this will reduce potential slow downs in the painting process.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.
  • Web page addresses and e-mail addresses turn into links automatically.

More information about formatting options

Nadeau software consulting
Nadeau software consulting