The first part of adding animations to Swing applications showed a simple non-rectangular window with an overlapping close button and translucent painting. In this entry i’m going to talk about showing an animated load progress indication while the application is connecting to the Amazon backend. This code is part of the Onyx project which aims to provide blueprints for animated Swing applications powered by the Trident animation library.

As a reminder, here is a screenshot of the main skeleton Onyx window:

While the previous part showed the code for the main window and the close button, it’s now time to look at the main album overview panel. For the demo purposes, the code is built in a layered fashion, with each layer adding more functional and animation behavior.

We start with the base class that provides the container translucency, fade in on becoming part of the host window and mouse drag:

public class Stage0Base extends JComponent {
   /**
    * The alpha value for this container. Is updated in the fade-in timeline
    * which starts when this container becomes a part of the host window
    * hierarchy.
    */
   float alpha;

   /**
    * Creates the basic container.
    */
   public Stage0Base() {
      this.setOpaque(false);
      this.alpha = 0.0f;

As in the previous entry, we have a non-opaque component with an alpha attribute set to zero during the initialization. To fade it in, we create a simple timeline that interpolates the alpha to 75% once the component becomes part of the window hierarchy:

      // fade in the container once it's part of the window
      // hierarchy
      this.addHierarchyListener(new HierarchyListener() {
         @Override
         public void hierarchyChanged(HierarchyEvent e) {
            Timeline shownTimeline = new Timeline(Stage0Base.this);
            shownTimeline.addPropertyToInterpolate("alpha", 0.0f, 0.75f);
            shownTimeline.addCallback(new Repaint(Stage0Base.this));
            shownTimeline.setDuration(500);
            shownTimeline.play();
         }
      });
   }

As with most modern non-rectangular application, the main Onyx demo allows dragging the main window by simply grabbing it with the mouse. To do this we add the following mouse adapter:

// mouse listener for dragging the host window
MouseAdapter adapter = new MouseAdapter() {
   int lastX;
   int lastY;

   @Override
   public void mousePressed(MouseEvent e) {
      Component source = (Component) e.getSource();

      Point eventLocationOnScreen = e.getLocationOnScreen();
      if (eventLocationOnScreen == null) {
         eventLocationOnScreen = new Point(e.getX()
            + source.getLocationOnScreen().x, e.getY()
            + source.getLocationOnScreen().y);
      }

      lastX = eventLocationOnScreen.x;
      lastY = eventLocationOnScreen.y;
   }

   @Override
   public void mouseDragged(MouseEvent e) {
      Component source = (Component) e.getSource();

      Point eventLocationOnScreen = e.getLocationOnScreen();
      if (eventLocationOnScreen == null) {
         eventLocationOnScreen = new Point(e.getX()
            + source.getLocationOnScreen().x, e.getY()
            + source.getLocationOnScreen().y);
      }

      int dx = eventLocationOnScreen.x - lastX;
      int dy = eventLocationOnScreen.y - lastY;
      Window win = SwingUtilities.getWindowAncestor(Stage0Base.this);
      Point loc = win.getLocation();
      win.setLocation(loc.x + dx, loc.y + dy);
      lastX = eventLocationOnScreen.x;
      lastY = eventLocationOnScreen.y;
   }
};
this.addMouseListener(adapter);
this.addMouseMotionListener(adapter);

We add a public setter for the alpha attribute so that it can be interpolated by Trident:

   public void setAlpha(float alpha) {
      this.alpha = alpha;
   }

and implement the simple painting based on the current alpha value:

   @Override
   protected void paintComponent(Graphics g) {
      Graphics2D g2d = (Graphics2D) g.create();

      g2d.setStroke(new BasicStroke(1.0f));
      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);

      int radius = 32;
      Shape contour = new RoundRectangle2D.Double(0, 0, getWidth() - 1,
            getHeight() - 1, radius, radius);
      g2d.setComposite(AlphaComposite.SrcOver.derive(this.alpha));
      g2d.setColor(new Color(0, 0, 0));
      g2d.fill(contour);
      g2d.setColor(Color.gray);
      g2d.draw(contour);

      g2d.dispose();
   }

The next layer adds the load progress indication. Here is how it looks under full opacity:

There are two separate attributes that control the load progress animation. The first controls the alpha, fading the load progress in on load start and fading it out on load end. The second controls the stripes offset and is responsible for creating a continuous indefinite appearance of “marching ants” progress. Each one is controlled by a separate timeline, and here we need to synchronize these two timelines:

  • On load start, we start both timelines.
  • On load end, we start the fade out timeline, and once it’s done, we cancel the looping “marching ants” timeline.

We start with the definitions of these two attributes and the matching timelines:

public class Stage1LoadingProgress extends Stage0Base {
   /**
    * The looping timeline to animate the indefinite load progress. When
    * {@link #setLoading(boolean)} is called with true, this
    * timeline is started. When {@link #setLoading(boolean)} is called with
    * false, this timeline is cancelled at the end of the
    * {@link #loadingBarFadeTimeline}.
    */
   Timeline loadingBarLoopTimeline;

   /**
    * The current position of the {@link #loadingBarLoopTimeline}.
    */
   float loadingBarLoopPosition;

   /**
    * The timeline for showing and hiding the loading progress bar. When
    * {@link #setLoading(boolean)} is called with true, this
    * timeline is started. When {@link #setLoading(boolean)} is called with
    * false, this timeline is started in reverse.
    */
   Timeline loadingBarFadeTimeline;

   /**
    * The current alpha value of the loading progress bar. Is updated by the
    * {@link #loadingBarFadeTimeline}.
    */
   float loadingBarAlpha;

and define the pixel dimensions of the load progress

   /**
    * The pixel width of the load progress visuals.
    */
   private static final int PROGRESS_WIDTH = 300;

   /**
    * The pixel height of the load progress visuals.
    */
   private static final int PROGRESS_HEIGHT = 32;

Now it’s time to initialize the attributes:

public Stage1LoadingProgress() {
   super();

   this.loadingBarLoopPosition = 0.0f;
   // create the looping timeline
   this.loadingBarLoopTimeline = new Timeline(this);
   this.loadingBarLoopTimeline.addPropertyToInterpolate(
         "loadingBarLoopPosition", 0.0f, 1.0f);
   this.loadingBarLoopTimeline.addCallback(new TimelineCallbackAdapter() {
      @Override
      public void onTimelinePulse(float durationFraction,
            float timelinePosition) {
         // don't repaint the whole window
         int x = (getWidth() - PROGRESS_WIDTH) / 2;
         int y = (getHeight() - PROGRESS_HEIGHT) / 2;
         Stage1LoadingProgress.this.repaint(x - 5, y - 5,
            PROGRESS_WIDTH + 10, PROGRESS_HEIGHT + 10);
      }
   });
   this.loadingBarLoopTimeline.setDuration(750);

This initializes the stripe location value to zero, and configures the looping timeline to interpolate it from zero to one. Later on this timeline will be played in an indefinite loop (cancelled once the load is done), and together with the matching painting code will result in a continuous visual appearance of indefinitely moving stripes. Note a custom repaint callback that only repaints the “dirty” area of the load progress, resulting in better CPU utilization during the load stage.

Now, it’s time to initialize the fading timeline:

this.loadingBarAlpha = 0.0f;
// create the fade timeline
this.loadingBarFadeTimeline = new Timeline(this);
this.loadingBarFadeTimeline.addPropertyToInterpolate("loadingBarAlpha",
      0.0f, 1.0f);
this.loadingBarFadeTimeline.addCallback(new TimelineCallbackAdapter() {
   @Override
   public void onTimelineStateChanged(TimelineState oldState,
         TimelineState newState, float durationFraction,
         float timelinePosition) {
      if (oldState == TimelineState.PLAYING_REVERSE
         && newState == TimelineState.DONE) {
         // after the loading progress is faded out, stop the loading
         // animation
         loadingBarLoopTimeline.cancel();
      }
   }
});
this.loadingBarFadeTimeline.setDuration(500);

In addition to interpolating the alpha value, it also cancels the looping timeline when the state changes from PLAYING_REVERSE to DONE – this signifies the end of the fade out sequence.

Adding the simple setters for the two float attributes:

/**
 * Sets the new alpha value of the loading progress bar. Is called by the
 * {@link #loadingBarFadeTimeline}.
 *
 * @param loadingBarAlpha
 *            The new alpha value of the loading progress bar.
 */
public void setLoadingBarAlpha(float loadingBarAlpha) {
   this.loadingBarAlpha = loadingBarAlpha;
}

/**
 * Sets the new loop position of the loading progress bar. Is called by the
 * {@link #loadingBarLoopTimeline}.
 *
 * @param loadingBarLoopPosition
 *            The new loop position of the loading progress bar.
 */
public void setLoadingBarLoopPosition(float loadingBarLoopPosition) {
   this.loadingBarLoopPosition = loadingBarLoopPosition;
}

it’s time for a very simple implementation of load state change:

   /**
    * Starts or stops the loading progress animation.
    *
    * @param isLoading
    *            if true, this container will display a loading
    *            progress animation, if false, the loading
    *            progress animation will be stopped.
    */
public void setLoading(boolean isLoading) {
   if (isLoading) {
      this.loadingBarFadeTimeline.play();
      this.loadingBarLoopTimeline.playLoop(RepeatBehavior.LOOP);
   } else {
      this.loadingBarFadeTimeline.playReverse();
   }
}

As said before, on load start both timelines start playing (note that the second one is played in a loop). On load end, the fade timeline is played in reverse – once it’s done, it will cancel the second looping timeline in the listener registered in its initialization.
Finally, the painting code respects both the alpha and the looping position. Note that it is done in the paintChildren method ensuring that the load progress is painted on top of all children:

@Override
protected void paintChildren(Graphics g) {
   super.paintChildren(g);

   if (this.loadingBarAlpha > 0.0f) {
      // paint the load progress over the children
      int x = (getWidth() - PROGRESS_WIDTH) / 2;
      int y = (getHeight() - PROGRESS_HEIGHT) / 2;

      Graphics2D g2d = (Graphics2D) g.create();
      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
      g2d.setComposite(AlphaComposite.SrcOver
            .derive(this.loadingBarAlpha));

      Shape currClip = g2d.getClip();
      g2d.clip(new RoundRectangle2D.Double(x, y, PROGRESS_WIDTH,
            PROGRESS_HEIGHT, 8, 8));
      g2d
         .setPaint(new LinearGradientPaint(x, y, x, y
               + PROGRESS_HEIGHT, new float[] { 0.0f, 0.49999f,
               0.5f, 1.0f }, new Color[] {
               new Color(156, 208, 221), new Color(101, 183, 243),
               new Color(67, 169, 241), new Color(138, 201, 247) }));
      g2d.fillRect(x, y, PROGRESS_WIDTH, PROGRESS_HEIGHT);

      int stripeCellWidth = 25;
      g2d.setPaint(new LinearGradientPaint(x, y, x, y + PROGRESS_HEIGHT,
         new float[] { 0.0f, 0.49999f, 0.5f, 1.0f }, new Color[] {
               new Color(36, 155, 239), new Color(17, 145, 238),
               new Color(15, 56, 200), new Color(3, 133, 219) }));
      g2d.setStroke(new BasicStroke(9.0f));
      for (int stripeX = x
         + (int) (this.loadingBarLoopPosition * stripeCellWidth); stripeX < x
         + PROGRESS_WIDTH + PROGRESS_HEIGHT; stripeX += stripeCellWidth) {
         g2d.drawLine(stripeX, y, stripeX - stripeCellWidth, y
               + PROGRESS_HEIGHT);
      }

      g2d.setClip(currClip);

      g2d.setColor(Color.lightGray);
      g2d.setStroke(new BasicStroke(1.3f));
      g2d.drawRoundRect(x, y, PROGRESS_WIDTH, PROGRESS_HEIGHT, 8, 8);

      g2d.dispose();
   }
}

Each load progress stripe is painted as a thick diagonal line, and the X offset of the first stripe is computed based on the current position of the looping timeline. The stripeCellWidth value indicates the horizontal distance between two adjacent stripes, and multiplying it by the current position of the looping timeline results in continuous indefinite progress:

for (int stripeX = x
   + (int) (this.loadingBarLoopPosition * stripeCellWidth); stripeX < x
   + PROGRESS_WIDTH + PROGRESS_HEIGHT; stripeX += stripeCellWidth) {
   g2d.drawLine(stripeX, y, stripeX - stripeCellWidth, y
         + PROGRESS_HEIGHT);
}

Here we have seen how to add animated load progress indication while the application is loading data. The next entry is going to talk about scrolling the album covers showed in the container and how to add animations to the scrolling.

In this first technical posting on adding animations to Swing applications i’m going to show a simple non-rectangular window with an overlapping close button and translucent painting. This code is part of the Onyx project which aims to provide blueprints for animated Swing applications powered by the Trident animation library.

Here is a screenshot of the main skeleton Onyx window:

It is a non-rectangular window with translucent painting and overlapping components. Here is a short walkthrough of the matching class:

public class MainWindow extends JFrame {
   AlbumOverviewPanel contentPanel;

   CloseButton closeButton;

It is a regular Swing JFrame with two components – the content panel and the close button. Let’s take a look at the constructor of this frame:

public MainWindow() {
   super("Onyx");
   this.setUndecorated(true);
   this.setBackground(new Color(0, 0, 0, 0));

It starts with setting the title for this frame, which is useful for identifying it in the taskbar. The next two lines set the frame to be undecorated (without the usual title pane) and translucent – setting the background color to a completely transparent color. This is a new API in JDK 7.

Next, we add the two components and set their Z order to have the close button painted last:

   this.contentPanel = new AlbumOverviewPanel();
   this.closeButton = new CloseButton();

   Container contentPane = this.getContentPane();
   contentPane.add(this.contentPanel);
   contentPane.add(this.closeButton);

   contentPane.setComponentZOrder(this.contentPanel, 1);
   contentPane.setComponentZOrder(this.closeButton, 0);

Next, we create a custom layout manager that sets the bounds for these two components. The close button is displayed in the top right corner, and the album overview panel spans the available size minus 10 pixels on top and on right:

   contentPane.setLayout(new LayoutManager() {
      @Override
      public void addLayoutComponent(String name, Component comp) {
      }

      @Override
      public void removeLayoutComponent(Component comp) {
      }

      @Override
      public Dimension minimumLayoutSize(Container parent) {
         return null;
      }

      @Override
      public Dimension preferredLayoutSize(Container parent) {
         return null;
      }

      @Override
      public void layoutContainer(Container parent) {
         int closeButtonDim = 35;
         closeButton.setBounds(getWidth() - closeButtonDim, 0,
            closeButtonDim, closeButtonDim);
         contentPanel
            .setBounds(0, 10, getWidth() - 10, getHeight() - 10);
      }
   });

Finally, we set the frame size and center it in the screen (ignore the window focus listener for now, it will be explained later):

   this.setSize(560, 210);
   this.setLocationRelativeTo(null);
}

This is it for the constructor. Now let’s take a look at the implementation of the close button. The close button has a rollover animation that displays a blueish outline and inner cross on acquiring the mouse:

It is an extension of the JButton class with a float field that stores the current alpha channel:

public class CloseButton extends JButton {
   /**
    * The alpha value of this button. Is updated in the fade-in timeline which
    * starts when this button becomes a part of the host window hierarchy.
    */
   float alpha;

The code uses the foreground attribute of the JComponent class and the matching setForeground method to provide the rollover animation. Let’s take a look at the constructor of the close button:

public CloseButton() {
   // mark the button as non-opaque since it will be
   // round shaped and translucent
   this.setOpaque(false);
   this.setForeground(Color.white);
   this.alpha = 0.0f;

It first marks the button as non-opaque and initializes the foreground to white. Then it sets the current alpha to zero so that we will fade in the button when it first appears on the screen. Next we add an action listener to close the ancestor window when this button is activated:

this.addActionListener(new ActionListener() {
   @Override
   public void actionPerformed(ActionEvent e) {
      SwingUtilities.invokeLater(new Runnable() {
         @Override
         public void run() {
            // dispose the host window
            Window windowAncestor = SwingUtilities
               .getWindowAncestor(CloseButton.this);
            windowAncestor.dispose();
         }
      });
   }
});

Now it’s time to create a rollover timeline that will interpolate the foreground color of the button (note how here we are relying on the presence of the JComponent.setForeground API):

// timeline for the rollover effect (interpolating the
// button's foreground color)
final Timeline rolloverTimeline = new Timeline(this);
rolloverTimeline.addPropertyToInterpolate("foreground", Color.white,
   new Color(64, 140, 255));
rolloverTimeline.setDuration(200);

And wire it to the mouse events on the button:

// and register a mouse listener to play the rollover
// timeline
this.addMouseListener(new MouseAdapter() {
   @Override
   public void mouseEntered(MouseEvent e) {
      rolloverTimeline.play();
   }

   @Override
   public void mouseExited(MouseEvent e) {
      rolloverTimeline.playReverse();
   }
});

Here, we are using the built-in Trident functionality that detects the current state of the timeline when the application code asks to play it forward or reverse. Suppose it takes two seconds to play a timeline, and you move the mouse out after one second. In this case, you do want the timeline to play back from its current position – and the other way around. Trident provides this functionality out of the box, and you do not need any additional application code or configuration.

Finally, we are going to add an hierarchy listener to fade in the button when it first is added to the window hierarchy:

// fade in the component once it's part of the window
// hierarchy
this.addHierarchyListener(new HierarchyListener() {
   @Override
   public void hierarchyChanged(HierarchyEvent e) {
      Timeline shownTimeline = new Timeline(CloseButton.this);
      shownTimeline.addPropertyToInterpolate("alpha", 0.0f, 1.0f);
      shownTimeline.addCallback(new Repaint(CloseButton.this));
      shownTimeline.setDuration(500);
      shownTimeline.play();
   }
});

Here, we also need a public setter for the alpha property since it is used in this timeline:

/**
 * Sets the alpha value. Used by the fade-in timeline.
 *
 * @param alpha
 *            Alpha value for this button.
 */
public void setAlpha(float alpha) {
   this.alpha = alpha;
}

Now that the button has been configured to fade in on appearance and interpolate its foreground on acquiring the mouse, we need to provide the custom painting based on these two values (alpha and foreground). We start with overriding the paintBorder to do nothing:

@Override
protected void paintBorder(Graphics g) {
   // overriden to remove the default border painting
}

And continue with the custom paintComponent method:

@Override
protected void paintComponent(Graphics g) {
   Graphics2D g2d = (Graphics2D) g.create();

   g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON);

   // use the current alpha
   g2d.setComposite(AlphaComposite.SrcOver.derive(this.alpha));

Here, after switching the anti-alias on, we are setting the composite based on the current alpha value. The rest of the painting operations will be affected by this alpha:

   // paint the background - black fill and a dark outline
   // based on the current foreground color
   Shape contour = new Ellipse2D.Double(1, 1, getWidth() - 3,
      getHeight() - 3);
   g2d.setColor(Color.black);
   g2d.setStroke(new BasicStroke(2.0f));
   g2d.fill(contour);
   g2d.setColor(this.getForeground().darker().darker());
   g2d.draw(contour);

   // paint the outer cross (always white)
   g2d.setColor(Color.white);
   g2d.setStroke(new BasicStroke(6.0f, BasicStroke.CAP_ROUND,
      BasicStroke.JOIN_ROUND));
   int offset = getWidth() / 3;
   g2d.drawLine(offset, offset, getWidth() - offset - 1, getHeight()
      - offset - 1);
   g2d.drawLine(getWidth() - offset - 1, offset, offset, getHeight()
      - offset - 1);

This code paints the black background and the white outer cross. Note how here we are using the current foreground color for the outer contour of the button – since setForeground calls repaint inside, on every step of the rollover timeline the foreground will be changed, and the paintComponent method will be called – effectively animating the outer contour from white to blue on mouse enter and from blue to white on mouse exit.

   // paint the inner cross (using the current foreground color)
   g2d.setColor(this.getForeground());
   g2d.setStroke(new BasicStroke(4.2f, BasicStroke.CAP_ROUND,
      BasicStroke.JOIN_ROUND));
   g2d.drawLine(offset, offset, getWidth() - offset - 1, getHeight()
      - offset - 1);
   g2d.drawLine(getWidth() - offset - 1, offset, offset, getHeight()
      - offset - 1);

   g2d.dispose();
}

Here we are painting the inner cross using the current foreground color – once again providing a smooth animated indication of acquiring or losing the mouse.

Now it is time to go back to the main window to show how it is disposed. Remember that this is the code in the close button:

this.addActionListener(new ActionListener() {
   @Override
   public void actionPerformed(ActionEvent e) {
      SwingUtilities.invokeLater(new Runnable() {
         @Override
         public void run() {
            // dispose the host window
            Window windowAncestor = SwingUtilities
               .getWindowAncestor(CloseButton.this);
            windowAncestor.dispose();
         }
      });
   }
});

It simply calls the dispose method on the host window. Here we do want to fade out the window on dispose (instead of abruptly hiding it). We are going to use the new setOpacity API available on the Window class in JDK 7 by overriding the dispose method in our main frame:

public void dispose() {
   Timeline dispose = new Timeline(this);
   dispose.addPropertyToInterpolate("opacity", 1.0f, 0.0f);
   dispose.addCallback(new UIThreadTimelineCallbackAdapter() {
      @Override
      public void onTimelineStateChanged(TimelineState oldState,
         TimelineState newState, float durationFraction,
         float timelinePosition) {
         if (newState == TimelineState.DONE) {
            superDispose();
         }
      }
   });
   dispose.setDuration(500);
   dispose.play();
}

Here, we start a new timeline that interpolates the opacity attribute of the window, which effectively fades it out. Once the timeline is done, we call the super implementation:

   private void superDispose() {
      super.dispose();
   }

Here we have seen how easy it is to add simple animation behavior to such scenarios as component appearance (fade in), rollovers and window disposal (fade-out) using built in and custom class attributes and setters. The next entry is going to show to talk about the base implementation of the album overview panel and the load progress animation.

Project Onyx aims to provide blueprints for adding animation to Swing applications using the Trident animation engine. Onyx is a Swing RIA that connects to Amazon E-commerce backend and shows a list of albums for a specific performer. It is a pure Swing / Java2D application with only two dependencies – Trident and the classes generated by the wsimport tool from the Amazon E-commerce WSDL.

Over the next few entries i’m going to show code snippets that illustrate how Trident can be used to add simple and complex animations to your Swing applications. The full code is in the SVN repository, and here are a few videos that show the different parts of Onyx in action:

Fading in the main Onyx window, rollover animations on the close button and fading out the main window on disposing:

Asynchronously connecting to Amazon to fetch and display the list of albums, with fading and looping load progress animation, and smooth album scrolling:

Showing bigger album art and scrollable track listing in a separate window when an album is selected, complete with a complex scenario of showing the art and switching between the albums:

Today, i’m happy to officially announce project Amber. In works over the last few weeks, it shows how to use Trident to add animations to an online interactive application that analyzes media trends. While previous demo videos showed some of Amber’s capabilities on test data sets, this high-definition video (watch in full-screen mode) shows Amber tracking the Digg tech news trends in real time.

To see Amber in action, click the WebStart button below and select one of the available Digg topics (last column):

Here are a few screenshots of Amber. The login screen (fades out on pressing the “Start” button):

The grid of story thumbnails (slide in and up):

Infotip showing story title and description (slides and fades in):

Another infotip, this time above the story thumbnail to fit in the frame:

Frequency chart of keywords in the shown entries – allows to analyze the popular topics:

The full code for Amber 1.0dev (code-named Ashberry) is available in the SVN repository, and Rémy Rakic (@lqd on Twitter) has already created a Twitter provider that allows analyzing public timelines:

To create a custom provider, take a look at the org.pushingpixels.amber.digg.DiggProvider class. One of the next blog entries will show another provider that uses the NY Times online APIs.