Fade animations

The org.jvnet.lafwidget.utils.FadeTracker class provides a utility for fade animations on component state changes. Its API allows tracking component model changes such as rollover effects (color fade-in / color fade-out), selection animations (animate checkbox selection), focus animations (focus ring fade-in / fade-out) and more.

The FadeKind inner class contains the following static instances for different cases of state changes:

  public static class FadeKind {

    /**
     * Arming a component.
     */
    public static final FadeKind ARM = new FadeKind("lafwidgets.core.arm");

    /**
     * Pressing a component.
     */
    public static final FadeKind PRESS = new FadeKind(
        "lafwidgets.core.press");

    /**
     * Focusing a component.
     */
    public static final FadeKind FOCUS = new FadeKind(
        "lafwidgets.core.focus");

    /**
     * Enabling a component.
     */
    public static final FadeKind ENABLE = new FadeKind(
        "lafwidgets.core.enable");

    /**
     * Rollover a component.
     */
    public static final FadeKind ROLLOVER = new FadeKind(
        "lafwidgets.core.rollover");

    /**
     * Selecting a component.
     */
    public static final FadeKind SELECTION = new FadeKind(
        "lafwidgets.core.selection");
  }

In addition, you can define your own instances of this class for custom animation kinds. Note - be sure to specify unique string in the constructor

The LafWidget.ANIMATION_KIND client property specified the animation kind. The value should be one of instances of org.jvnet.lafwidget.utils.LafConstants.AnimationKind class (DEBUG, NONE, SLOW, default NORMAL or FAST). This property can be set on a component, container (valid for all contained components unless some component has this property set) or UIManager (globally). The value affects the animation cycle speed (one step for NONE, four steps for FAST, five steps for NORMAL, ten steps for SLOW and hundred steps for DEBUG).

The API of FadeTracker class consists of the following sections:


Getting fade tracker instance   /**
   * Gets singleton instance.
   
   @return Singleton instance.
   */
  public synchronized static FadeTracker getInstance()

Commencing fade in / fade out   /**
   * Requests start of fade-in tracking on the specified component.
   
   @param fadeKind
   *            Fade kind.
   @param comp
   *            The component to track.
   @param toRepaintParent
   *            Indication whether the component parent should be repainted.
   @param callback
   *            Optional callback to be called on each fade cycle.
   */
  public synchronized void trackFadeIn(FadeKind fadeKind, Component comp,
      boolean toRepaintParent, FadeTrackerCallback callback)

  /**
   * Requests start of fade-in tracking on the specified component.
   
   @param fadeKind
   *            Fade kind.
   @param comp
   *            The component to track.
   @param componentId
   *            Component id. Relevant for such components as tabbed panes
   *            (where fade is performed on component "sub" parts).
   @param toRepaintParent
   *            Indication whether the component parent should be repainted.
   @param callback
   *            Optional callback to be called on each fade cycle.
   */
  public synchronized void trackFadeIn(FadeKind fadeKind, Component comp,
      int componentId, boolean toRepaintParent,
      FadeTrackerCallback callback)

  /**
   * Requests start of fade-in tracking on the specified component.
   
   @param fadeKind
   *            Fade kind.
   @param comp
   *            The component to track.
   @param componentId
   *            Component id. Relevant for such components as tabbed panes
   *            (where fade is performed on component "sub" parts).
   @param toRepaintParent
   *            Indication whether the component parent should be repainted.
   @param callback
   *            Optional callback to be called on each fade cycle.
   */
  public synchronized void trackFadeIn(FadeKind fadeKind, Component comp,
      Comparable componentId, boolean toRepaintParent,
      FadeTrackerCallback callback)

  /**
   * Requests start of fade-out tracking on the specified component.
   
   @param fadeKind
   *            Fade kind.
   @param comp
   *            The component to track.
   @param toRepaintParent
   *            Indication whether the component parent should be repainted.
   @param callback
   *            Optional callback to be called on each fade cycle.
   */
  public synchronized void trackFadeOut(FadeKind fadeKind, Component comp,
      boolean toRepaintParent, FadeTrackerCallback callback)

  /**
   * Requests start of fade-out tracking on the specified component.
   
   @param fadeKind
   *            Fade kind.
   @param comp
   *            The component to track.
   @param componentId
   *            Component id. Relevant for such components as tabbed panes
   *            (where fade is performed on component "sub" parts).
   @param toRepaintParent
   *            Indication whether the component parent should be repainted.
   @param callback
   *            Optional callback to be called on each fade cycle.
   */
  public synchronized void trackFadeOut(FadeKind fadeKind, Component comp,
      int componentId, boolean toRepaintParent,
      FadeTrackerCallback callback)

  /**
   * Requests start of fade-out tracking on the specified component.
   
   @param fadeKind
   *            Fade kind.
   @param comp
   *            The component to track.
   @param componentId
   *            Component id. Relevant for such components as tabbed panes
   *            (where fade is performed on component "sub" parts).
   @param toRepaintParent
   *            Indication whether the component parent should be repainted.
   @param callback
   *            Optional callback to be called on each fade cycle.
   */
  public synchronized void trackFadeOut(FadeKind fadeKind, Component comp,
      Comparable componentId, boolean toRepaintParent,
      FadeTrackerCallback callback)

Checking if a component is in fade state   /**
   * Checks whether the specified component is being tracked by
   <code>this</code> tracker. Effectively returns indication whether the
   * specified component is in fade-in or fade-out animation of the specified
   * kind.
   
   @param comp
   *            Component.
   @param fadeKind
   *            Fade kind.
   @return <code>true</code> if the specified component is being tracked
   *         by <code>this</code> tracker, <code>false</code> otherwise.
   */
  public boolean isTracked(Component comp, FadeKind fadeKind)

  /**
   * Checks whether the specified component is being tracked by
   <code>this</code> tracker. Effectively returns indication whether the
   * specified component is in fade-in or fade-out animation of the specified
   * kind.
   
   @param comp
   *            Component.
   @param componentId
   *            Component id. Relevant for such components as tabbed panes
   *            (where fade is performed on component "sub" parts).
   @param fadeKind
   *            Fade kind.
   @return <code>true</code> if the specified component is being tracked
   *         by <code>this</code> tracker, <code>false</code> otherwise.
   */
  public boolean isTracked(Component comp, int componentId, FadeKind fadeKind)

  /**
   * Checks whether the specified component is being tracked by
   <code>this</code> tracker. Effectively returns indication whether the
   * specified component is in fade-in or fade-out animation of the specified
   * kind.
   
   @param comp
   *            Component.
   @param componentId
   *            Component id. Relevant for such components as tabbed panes
   *            (where fade is performed on component "sub" parts).
   @param fadeKind
   *            Fade kind.
   @return <code>true</code> if the specified component is being tracked
   *         by <code>this</code> tracker, <code>false</code> otherwise.
   */
  public boolean isTracked(Component comp, Comparable componentId,
      FadeKind fadeKind)

Getting fade state of a component   /**
   * Returns the fade cycle for the specified component. The result will be in
   * 0.0-10.0 range.
   
   @param comp
   *            Component.
   @param fadeKind
   *            Fade kind.
   @return The fade cycle for the specified component. For components that
   *         are not tracked (when
   *         {@link #isTracked(Component, org.jvnet.lafwidget.utils.FadeTracker.FadeKind)}
   *         returns <code>false</code>), value 0 (zero) is returned.
   */
  public synchronized float getFade10(Component comp, FadeKind fadeKind)

  /**
   * Returns the fade cycle for the specified component. The result will be in
   * 0.0-10.0 range.
   
   @param comp
   *            Component.
   @param componentId
   *            Component id. Relevant for such components as tabbed panes
   *            (where fade is performed on component "sub" parts).
   @param fadeKind
   *            Fade kind.
   @return The fade cycle for the specified component. For components that
   *         are not tracked (when
   *         {@link #isTracked(Component, org.jvnet.lafwidget.utils.FadeTracker.FadeKind)}
   *         returns <code>false</code>), value 0 (zero) is returned.
   */
  public synchronized float getFade10(Component comp, int componentId,
      FadeKind fadeKind)

  /**
   * Returns the fade cycle for the specified component. The result will be in
   * 0.0-10.0 range.
   
   @param comp
   *            Component.
   @param componentId
   *            Component id. Relevant for such components as tabbed panes
   *            (where fade is performed on component "sub" parts).
   @param fadeKind
   *            Fade kind.
   @return The fade cycle for the specified component. For components that
   *         are not tracked (when
   *         {@link #isTracked(Component, org.jvnet.lafwidget.utils.FadeTracker.FadeKind)}
   *         returns <code>false</code>), value 0 (zero) is returned.
   */
  public synchronized float getFade10(Component comp, Comparable componentId,
      FadeKind fadeKind)

The FadeTrackerCallback interface is:

  /**
   * Callback for the fade tracker. Is used when the application (some UI
   * delegate) wishes to execute some code on the fade.
   
   @author Kirill Grouchnikov
   */
  public static interface FadeTrackerCallback {
    /**
     * Indicates that a single cycle of fade has been performed. Can be used
     * in order to repaint specific portion of the component.
     */
    public void fadePerformed(FadeKind fadeKind);
  }

It allows attaching a component-specific logic to be executed at fade steps. Here is an example from Substance tabbed pane UI delegate that illustrates the above API. We add a mouse motion listener on the tabbed pane. At each mouseMoved event, we check to see if the current "rollover" tab (tab under mouse) has changed. If it has, we need to fade-out the previous rollover tab and fade-in the current rollover tab. However, we do not wish to repaint the whole tabbed pane on each fade step - only these two tabs need to be repainted.

First, we implement the FadeTrackerCallback:

  protected class TabRepaintCallback implements FadeTrackerCallback {
    protected int tabIndex;

    protected JTabbedPane tabbedPane;

    public TabRepaintCallback(JTabbedPane tabComponent, int tabIndex) {
      this.tabbedPane = tabComponent;
      this.tabIndex = tabIndex;
    }

    public void fadePerformed(FadeKind fadeKind) {
      SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          ensureCurrentLayout();
          int tabCount = tabPane.getTabCount();
          if ((tabCount > 0&& (tabIndex < tabCount)
              && (tabIndex < rects.length)) {
            // need to retrieve the tab rectangle since the tabs
            // can be moved while animating (especially when the
            // current layout is SCROLL_LAYOUT)
            Rectangle rect = getTabBounds(tabPane, tabIndex);
            // System.out.println("Repainting " + tabIndex);
            tabPane.repaint(rect);
          }
        }
      });
    }
  }

Some bookkeeping is here in order to ensure that we will be animating the correct tab screen rectangle (since the tab rectangle can move during the animation cycle). Now we need to attach the relevant mouse listener to the tabbed pane itself (code not relevant to the animation fade omitted):

  protected class MouseRolloverHandler implements MouseListener,
      MouseMotionListener {
    int prevRolledOver = -1;

    public void mouseMoved(MouseEvent e) {
      mouseLocation = e.getPoint();
      int currRolledOver = getRolloverTab();
      if (currRolledOver == prevRolledOver) {
      else {
        FadeTracker fadeTracker = FadeTracker.getInstance();
        if ((prevRolledOver >= 0)
            && (prevRolledOver < tabPane.getTabCount())
            && tabPane.isEnabledAt(prevRolledOver)) {
          fadeTracker.trackFadeOut(FadeKind.ROLLOVER, tabPane,
              prevRolledOver, true, new TabRepaintCallback(
                  tabPane, prevRolledOver));
        }
        if ((currRolledOver >= 0)
            && (currRolledOver < tabPane.getTabCount())
            && tabPane.isEnabledAt(currRolledOver)) {
          fadeTracker.trackFadeIn(FadeKind.ROLLOVER, tabPane,
              currRolledOver, true, new TabRepaintCallback(
                  tabPane, currRolledOver));
        }
      }
      prevRolledOver = currRolledOver;
    }

    public void mouseExited(MouseEvent e) {
      if ((prevRolledOver >= 0)
          && (prevRolledOver < tabPane.getTabCount())
          && tabPane.isEnabledAt(prevRolledOver)) {
        // only the previously rolled-over tab needs to be
        // repainted (fade-out) instead of repainting the
        // whole tab as before.
        FadeTracker fadeTracker = FadeTracker.getInstance();
        fadeTracker.trackFadeOut(FadeKind.ROLLOVER, tabPane,
            prevRolledOver, true, new TabRepaintCallback(tabPane,
                prevRolledOver));
      }
      prevRolledOver = -1;
    }
  }

As can be seen, the prevRolledOver contains the index of the previous rollover tab. If the rollover tab changes, we ask the fade tracker to fade out the previous rollover tab and fade in the new rollover tab. The mouseExited implementation requests the fade-out of the previous (in this case it's actually current) rollover tab.

Now that we post the requests on the fade tracker, we need to change the painting code accordingly. In the overriding implementation of paintTabBackground method, we call the fade tracker API to see if the specific tab is tracked, and if so, what is its current fade cycle (see code below). The fade cycle is a value from 0.0 to 10.0 and is used as an interpolation coefficient between metal and colored backgrounds. This way we simulate the animated transition from either unselected (metal) to selected (color) or from selected (color) to unselected (metal) state. The relevant code snippet is:

      if (fadeTracker.isTracked(this.tabPane, tabIndex, null)) {
        ColorScheme metallicScheme = SubstanceLookAndFeel
            .getMetallicColorScheme();
        if (!isRollover) {
          // Came from rollover state
          colorScheme2 = SubstanceLookAndFeel.getColorScheme();
          colorScheme = isSelected ? colorScheme : metallicScheme;
          cyclePos = fadeTracker
              .getFade10(this.tabPane, tabIndex, null);
        else {
          // Came from default state
          colorScheme2 = colorScheme;
          colorScheme = isSelected ? colorScheme : metallicScheme;
          cyclePos = fadeTracker
              .getFade10(this.tabPane, tabIndex, null);
        }
      }

The colorScheme and colorScheme2 are used later to create the actual background (for animated tabs one of them points to metal scheme and the other points to a color scheme). The important variable here is cyclePos that returns the fade cycle position for the specific tab. This value is later used to interpolate between the metal and color schemes. At each fade cycle, the custom fade callback shown earlier (which calls repaint on the specific tab rectangle) is called. Eventually, we get to the paintTabBackground which interpolates the background according to the current fade cycle. At the beginning of the fade-in, the tab is metal, at the end the tab is colored, and in the middle it is a blend of two schemes.

For more common cases (when the entire component should be repainted during the fading cycle), you can use the org.jvnet.lafwidget.utils.FadeStateListener helper class. This class provides tracking of all changes to button-deriven components (buttons, toggle buttons, check boxes, radio buttons), including the model itself (selected, pressed, armed, rollover, enabled) and the focus gain / loss. The relevant API is (see below for code samples):

  /**
   * Creates a new listener on model changes that can cause fade animation
   * transitions.
   
   @param comp
   *            Component.
   @param buttonModel
   *            Model for the component.
   @param callback
   *            Optional application callback.
   */
  public FadeStateListener(Component comp, ButtonModel buttonModel,
      FadeTrackerCallback callback)

  /**
   * Registers listeners on all model changes.
   */
  public void registerListeners()

  /**
   * Unregisters all listeners on model changes.
   */
  public void unregisterListeners()

If you decide to use this class in one of your UI delegates, you will need to perform the following steps:


Add new field to the UI delegate   protected FadeStateListener fadeStateListener;

Wire it in the installListeners()   @Override
  protected void installListeners(AbstractButton b) {
    super.installListeners(b);

    this.fadeStateListener = new FadeStateListener(b, b.getModel()null);
    this.fadeStateListener.registerListeners();
  }

Unwire it in the uninstallListeners()   @Override
  protected void uninstallListeners(AbstractButton b) {
    this.fadeStateListener.unregisterListeners();
    this.fadeStateListener = null;

    super.uninstallListeners(b);
  }

Use the fade tracker in the painting code     // check if fading
    FadeTracker fadeTracker = FadeTracker.getInstance();
    float visibility = state.isSelected() 10.0f 0.0f;
    if (fadeTracker.isTracked(button, FadeKind.SELECTION)) {
      visibility = fadeTracker.getFade10(button, FadeKind.SELECTION);
    }

In the painting code above, the visibility is later used to set the opacity of the radio button mark. This code allows fade animations on selecting / deselecting the radio button (the radio mark appears / disappears smoothly). The same principle is easily extended to rollover, enabled and focus animations.