Animation blueprints for SWT – showing load progress

August 8th, 2009

The first part of adding animations to SWT applications showed a simple window with an overlapping close button and animated fade-in / fade-out sequences. 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 Granite project which aims to provide blueprints for animated SWT applications powered by the Trident animation library.

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

and this is how it looks like when the application has loaded album details from Amazon E-commerce backend:

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 functionality of dragging the entire application window with the mouse:

public class Stage0Base extends Canvas {
   /**
    * Creates the basic container.
    */
   public Stage0Base(final Composite composite) {
      super(composite, SWT.DOUBLE_BUFFERED);

      Listener l = new Listener() {
         Point origin;

         public void handleEvent(Event e) {
            switch (e.type) {
            case SWT.MouseDown:
               origin = new Point(e.x, e.y);
               break;
            case SWT.MouseUp:
               origin = null;
               break;
            case SWT.MouseMove:
               if (origin != null) {
                  Shell shell = composite.getShell();
                  Point p = shell.getDisplay().map(shell, null, e.x, e.y);
                  shell.setLocation(p.x - origin.x, p.y - origin.y);
               }
               break;
            }
         }
      };
      this.addListener(SWT.MouseDown, l);
      this.addListener(SWT.MouseUp, l);
      this.addListener(SWT.MouseMove, l);
   }
}

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

https://pushingpixels.dev.java.net/images/granite/albumloading.png

And here it is when it’s fading out once the albums has been loaded (note how it overlays the loaded album cover art):

https://pushingpixels.dev.java.net/images/granite/loadingbarfadingout.png

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.

The progress bar is implemented as a custom SWT control that extends the Canvas class. This control tracks both the position of the progress and the alpha value of the progress bar. Here is the skeleton of the progress bar control, along with the matching attribute declarations:

   protected static class ProgressBarIndicator extends Canvas {
      /**
       * The current position of the {@link #loadingBarLoopTimeline}.
       */
      float loadingBarLoopPosition;

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

      public ProgressBarIndicator(Composite composite) {
         super(composite, SWT.TRANSPARENT | SWT.DOUBLE_BUFFERED
               | SWT.NO_BACKGROUND);
      /**
       * 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(int 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;
      }
   }

The main class declares an instance of the progress bar, as well as the two 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 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 pixel width of the load progress visuals.
    */
   static final int PROGRESS_WIDTH = 300;

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

   protected ProgressBarIndicator progressIndicator;

The constructor of the main class defines the custom layout manager that centers the progress bar in its parent, as well as the two timelines:

   /**
    * Creates a container with support for showing load progress.
    */
   public Stage1LoadingProgress(Composite composite) {
      super(composite);

      this.progressIndicator = new ProgressBarIndicator(this);
      this.setLayout(new Layout() {
         @Override
         protected void layout(Composite composite, boolean flushCache) {
            int w = composite.getBounds().width;
            int h = composite.getBounds().height;
            // put the progress indication in the center
            progressIndicator.setBounds((w - PROGRESS_WIDTH) / 2,
                  (h - PROGRESS_HEIGHT) / 2, PROGRESS_WIDTH,
                  PROGRESS_HEIGHT);
         }

         @Override
         protected Point computeSize(Composite composite, int wHint,
               int hHint, boolean flushCache) {
            return new Point(wHint, hHint);
         }
      });

      this.loadingBarLoopTimeline = new Timeline(this.progressIndicator);
      this.loadingBarLoopTimeline.addPropertyToInterpolate(
            "loadingBarLoopPosition", 0.0f, 1.0f);
      this.loadingBarLoopTimeline.addCallback(new SWTRepaintCallback(this));
      this.loadingBarLoopTimeline.setDuration(750);

      // create the fade timeline
      this.loadingBarFadeTimeline = new Timeline(this.progressIndicator);
      this.loadingBarFadeTimeline.addPropertyToInterpolate("loadingBarAlpha",
            0, 255);
      this.loadingBarFadeTimeline
            .addCallback(new UIThreadTimelineCallbackAdapter() {
               @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();
                     progressIndicator.setVisible(false);
                  }
               }
            });
      this.loadingBarFadeTimeline.setDuration(500);
   }

The looping timeline is configured to interpolate the stripe location 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. The fade timeline interpolates 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.

Now 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 – this is done in a custom paint listener registered on the progress bar component:

         this.addPaintListener(new PaintListener() {
            @Override
            public void paintControl(PaintEvent e) {
               if (loadingBarAlpha > 0) {
                  int width = getBounds().width;
                  int height = getBounds().height;

                  GC gc = e.gc;
                  gc.setAntialias(SWT.ON);
                  gc.setAlpha(loadingBarAlpha);

                  Region clipping = new Region(e.display);
                  gc.getClipping(clipping);

                  int contourRadius = 8;

                  // create a round rectangle clip to paint the inner part
                  // of the progress indicator
                  Path clipPath = new GraniteUtils.RoundRectangle(
                        e.display, 0, 0, width, height, contourRadius);

                  gc.setClipping(clipPath);

                  Color fill1 = new Color(e.display, 156, 208, 221);
                  Color fill2 = new Color(e.display, 101, 183, 243);
                  Pattern pFill1 = new Pattern(e.display, 0, 0, 0,
                        height / 2.0f, fill1, loadingBarAlpha, fill2,
                        loadingBarAlpha);
                  gc.setBackgroundPattern(pFill1);
                  gc.fillRectangle(0, 0, width, height / 2);
                  fill1.dispose();
                  fill2.dispose();
                  pFill1.dispose();

                  Color fill3 = new Color(e.display, 67, 169, 241);
                  Color fill4 = new Color(e.display, 138, 201, 247);
                  Pattern pFill2 = new Pattern(e.display, 0,
                        height / 2.0f, 0, height, fill3,
                        loadingBarAlpha, fill4, loadingBarAlpha);
                  gc.setBackgroundPattern(pFill2);
                  gc.fillRectangle(0, height / 2, width, height / 2);
                  fill3.dispose();
                  fill4.dispose();
                  pFill2.dispose();

                  int stripeCellWidth = 25;
                  Color stripe1 = new Color(e.display, 36, 155, 239);
                  Color stripe2 = new Color(e.display, 17, 145, 238);
                  Pattern pStripe1 = new Pattern(e.display, 0, 0, 0,
                        height / 2.0f, stripe1, loadingBarAlpha,
                        stripe2, loadingBarAlpha);
                  Color stripe3 = new Color(e.display, 15, 56, 200);
                  Color stripe4 = new Color(e.display, 3, 133, 219);
                  Pattern pStripe2 = new Pattern(e.display, 0, 0, 0,
                        height / 2.0f, stripe3, loadingBarAlpha,
                        stripe4, loadingBarAlpha);

                  int stripeWidth = 10;
                  gc.setLineAttributes(new LineAttributes(9.0f));
                  for (int stripeX = (int) (loadingBarLoopPosition * stripeCellWidth); stripeX < width
                        + height; stripeX += stripeCellWidth) {
                     gc.setBackgroundPattern(pStripe1);
                     gc.fillPolygon(new int[] {
                           stripeX - stripeWidth / 2,
                           0,
                           stripeX + stripeWidth / 2,
                           0,
                           stripeX - stripeCellWidth / 2 + stripeWidth
                                 / 2,
                           height / 2,
                           stripeX - stripeCellWidth / 2 - stripeWidth
                                 / 2, height / 2 });
                     gc.setBackgroundPattern(pStripe2);
                     gc
                           .fillPolygon(new int[] {
                                 stripeX - stripeCellWidth / 2
                                       - stripeWidth / 2,
                                 height / 2,
                                 stripeX - stripeCellWidth / 2
                                       + stripeWidth / 2,
                                 height / 2,
                                 stripeX - stripeCellWidth
                                       + stripeWidth / 2,
                                 height,
                                 stripeX - stripeCellWidth
                                       - stripeWidth / 2, height });
                  }
                  stripe1.dispose();
                  stripe2.dispose();
                  stripe3.dispose();
                  stripe4.dispose();
                  pStripe1.dispose();
                  pStripe2.dispose();

                  // restore the original clipping to paint the contour
                  gc.setClipping(clipping);
                  clipping.dispose();

                  gc.setForeground(e.display
                        .getSystemColor(SWT.COLOR_GRAY));
                  float lineWeight = 1.6f;
                  gc.setLineAttributes(new LineAttributes(lineWeight));

                  Path outline = new GraniteUtils.RoundRectangle(
                        e.display, lineWeight / 2.0f - 1,
                        lineWeight / 2.0f - 1, width - 1 - lineWeight
                              + 2, height - 1 - lineWeight + 2,
                        contourRadius - lineWeight / 2);

                  gc.drawPath(outline);

                  outline.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.

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.