Animation blueprints for SWT – scrolling layout

August 10th, 2009

After adding such animation effects as fading, load progress and asynchronous load of images in the application window connected to the Amazon backend, it’s time to talk about smooth scrolling of the display results. In this entry i’m going to talk about loading the album art matching the specific search string and asynchronous display of the associated images. This code is part of the Granite project which aims to provide blueprints for animated SWT applications powered by the Trident animation library.

Here is a screenshot that illustrates the search results displayed as album art (and you can view the videos in the first part of this series):

The album items can be scrolled with right / left arrow keys, as well as by using the mouse wheel.

The base implementation of the layout does not use animations on the scrolling, but lays out the groundwork. It starts off by having the list of all album items, and the position of the currently leading item:

public class Stage2Components extends Stage1LoadingProgress {
   /**
    * The list of album overview components. Each component added with
    * {@link #addOverviewComp(Item, ActivationCallback)} is added to this list.
    */
   List comps;

   /**
    * Indicates which album overview component is displayed at the left edge of
    * this container. Note that while this specific class (in its
    * {@link #scrollToNext()} and {@link #scrollToPrevious()}) operate on the
    * integer values, the animated scrolling will result in fractional values
    * of the leading position.
    *
    * At the beginning the value is 0.0 - displaying the first entry in
    * {@link #comps} at the left edge. When scrolling to the next album, the
    * value will become 1.0 (effectively pushing the first album over the left
    * edge). If the scrolling is animated, this value will be gradually
    * interpolated from 0.0 to 1.0.
    *
    * This value is respected in the layout manager to provide the seamless
    * scroll animation.
    *
    */
   float leadingPosition;

   /**
    * Contains all the album components.
    */
   Composite albumComponentsHolder;

In this class, the leadingPosition will only have discrete integer values – immediate scrolling. However, we define is as float for the animation purposes (see below).

The constructor of this class creates the list for holding the album items, as well as registering relevant mouse wheel listener, input map and action map:

   /**
    * Creates the new container that can host album overview components.
    *
    * @param composite
    *            Parent composite.
    */
   public Stage2Components(Composite composite) {
      super(composite);
      this.comps = new ArrayList();

      this.albumComponentsHolder = new Composite(this, SWT.TRANSPARENT);
      this.progressIndicator.moveAbove(this.albumComponentsHolder);

      // register the mouse wheel listener for scrolling content
      composite.addMouseWheelListener(new MouseWheelListener() {
         @Override
         public void mouseScrolled(MouseEvent e) {
            if (e.count < 0) {
               // next
               scrollToNext();
            } else {
               // previous
               scrollToPrevious();
            }
         }
      });

      // use arrow keys for scrolling
      composite.getDisplay().addFilter(SWT.KeyUp, new Listener() {
         @Override
         public void handleEvent(Event event) {
            // only scroll when the details window is not showing
            if (event.keyCode == SWT.ARROW_RIGHT) {
               scrollToNext();
            }
            if (event.keyCode == SWT.ARROW_LEFT) {
               scrollToPrevious();
            }
         }
      });
   }

Next, a method to add a single album to this container:

   /**
    * Adds the specified album item to this album container.
    *
    * @param albumItem
    *            Description of the album item from the Amazon backend.
    * @return Thew matching album overview component.
    */
   public synchronized AlbumOverviewComponent addAlbumItem(Item albumItem) {
      AlbumOverviewComponent comp = new AlbumOverviewComponent(
            this.albumComponentsHolder, albumItem);
      this.comps.add(comp);
      this.albumComponentsHolder.layout(new Control[] { comp });
      return comp;
   }

And the implementation of discrete scrolling:

 
   /**
    * Scrolls the albums to show the next album.
    */
   protected void scrollToNext() {
      if (this.leadingPosition < (this.comps.size() - 1)) {
         this.leadingPosition++;
         this.albumComponentsHolder.layout(true);
      }
   }

   /**
    * Scrolls the albums to show the previous album.
    */
   protected void scrollToPrevious() {
      if (this.leadingPosition > 0) {
         this.leadingPosition--;
         this.albumComponentsHolder.layout(true);
      }
   }

The layout itself (triggered by the calls to layout() above) is quite simple – computing the X position of each album item based on the current leadingPosition:

      // set custom layout manager to arrange the row of album
      // components
      this.albumComponentsHolder.setLayout(new Layout() {
         @Override
         protected Point computeSize(Composite composite, int wHint,
               int hHint, boolean flushCache) {
            return new Point(wHint, hHint);
         }

         @Override
         protected void layout(Composite composite, boolean flushCache) {
            if (comps.size() == 0)
               return;

            for (int i = 0; i < comps.size(); i++) {
               float delta = i - leadingPosition;
               // compute the left X based on the current leading position
               int x = (int) (delta * (AlbumOverviewComponent.DEFAULT_WIDTH + 10));
               comps
                     .get(i)
                     .setBounds(
                           x,
                           (getBounds().height - AlbumOverviewComponent.DEFAULT_HEIGHT) / 2,
                           AlbumOverviewComponent.DEFAULT_WIDTH,
                           AlbumOverviewComponent.DEFAULT_HEIGHT);
            }
         }
      });

Now it’s time to add the scrolling animation to the mix. With the groundwork laid out already, the implementation is quite simple.

public class Stage3AnimatedScrolling extends Stage2Components {
   /**
    * Contains the target leading position - this is the index of the album
    * which should appear at the left edge once the current
    * {@link #scrollTimeline} is done. Note that the user scrolling can be done
    * in the middle of the current scrolling animation. In this case, the field
    * is updated with the new target index.
    */
   float targetLeadingPosition;

   /**
    * The scroll timeline. Note that the user scrolling can be done in the
    * middle of the current scrolling animation. In this case, the current
    * timeline is cancelled, and a new one is created - this allows quick
    * scrolling with multiple mouse wheel / arrow events.
    */
   Timeline scrollTimeline;

The targetLeadingPosition allows us to handle multiple consecutive scrolling requests (user quickly scrolling the mouse wheel) without having multiple timelines “competing” to scroll the album items.

The constructor doesn’t do anything special:

   /**
    * Creates the new container that can animate the album scrolling.
    *
    * @param composite
    *            Parent composite.
    */
   public Stage3AnimatedScrolling(Composite composite) {
      super(composite);
      this.targetLeadingPosition = 0;
   }

Now we get to the “meat” of this class – adding the scrolling animations. First, we override the scrolling methods from the super class to update the target position:

   @Override
   protected void scrollToNext() {
      if (this.targetLeadingPosition < (this.comps.size() - 1)) {
         this.targetLeadingPosition++;
         scrollContents();
      }
   }

   @Override
   protected void scrollToPrevious() {
      if (this.targetLeadingPosition > 0) {
         this.targetLeadingPosition--;
         scrollContents();
      }
   }

Where the scrollContents aborts the existing timeline (if necessary), and plays a new one:

   /**
    * Scrolls the contents of this container.
    */
   private synchronized void scrollContents() {
      if (this.scrollTimeline != null) {
         // abort the playing scroll timeline
         this.scrollTimeline.abort();
      }

      // and dynamically create a new one to change the
      // leading position
      this.scrollTimeline = new Timeline(this);
      this.scrollTimeline
            .addPropertyToInterpolate(Timeline. property(
                  "leadingPosition").fromCurrent().to(
                  this.targetLeadingPosition));
      this.scrollTimeline.setDuration(250);

      this.scrollTimeline.play();
   }

Canceling the old timeline makes sure that we will not have multiple timeline updating the leadingPosition field. And the targetLeadingPosition field holds the final value of the leadingPosition – or at least until the user makes an additional scrolling request.

Finally, we have a public setter and getter so that the Trident engine can change the value of the leadingPosition field:

   /**
    * Sets the new value for the leading position. This is called from
    * {@link #scrollTimeline}.
    * 
    * @param leadingPosition
    *            The new value for the leading position.
    */
   public void setLeadingPosition(float leadingPosition) {
      this.leadingPosition = leadingPosition;
      this.albumComponentsHolder.layout(true);
   }

   /**
    * Returns the current value of the leading position. This is called from
    * {@link #scrollTimeline}.
    * 
    * @return The current value of the leading position.
    */
   public float getLeadingPosition() {
      return this.leadingPosition;
   }

Here we have seen how to scroll the album covers showed in the container and how to add animations to the scrolling. The next entry is going to talk about displaying larger album art and scrollable track listing when the specific album is selected.