Animation blueprints for Swing – showing load progress

May 18th, 2009

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.