Trident animation library for Java applications is nearing release 1.1 (code-named Bogeyman), and it’s time to take a look at the new APIs added in this version. This entry is going to talk about supporting Java UI toolkits.

UI toolkit handlers

Graphical applications are a natural fit for animations, and Trident core has built-in support for Swing and SWT. This support covers threading rules, custom property interpolators and repaint timelines. Application code that needs to support additional Java-based UI toolkits should register a custom UI toolkit handler.

Most modern UI toolkits have threading rules that the applications must respect in order to prevent application freeze and visual artifacts. The threading rules for both Swing and SWT specify that the UI-related operations must be done on a special UI thread, and the methods in the org.pushingpixels.trident.UIToolkitHandler are used to determine the relevance of these threading rules. The UIToolkitHandler interface is illustrated by the core support for SWT:

public class SWTToolkitHandler implements UIToolkitHandler {
	@Override
	public boolean isHandlerFor(Object mainTimelineObject) {
		return (mainTimelineObject instanceof Widget);
	}

	@Override
	public boolean isInReadyState(Object mainTimelineObject) {
		return !((Widget) mainTimelineObject).isDisposed();
	}

	@Override
	public void runOnUIThread(Runnable runnable) {
		Display.getDefault().asyncExec(runnable);
	}
}

This is a very simple implementation of a UI toolkit handler that respects the relevant threading rules:

  • The isHandlerFor associates this handler with all SWT widgets
  • The isInReadyState marks disposed widgets to skip the property interpolation / callback invocations
  • The runOnUIThread runs the UI related logic on the SWT thread

Registering custom UI toolkit handlers

Trident provides two ways to register custom UI toolkit handlers – customization APIs and plugins.

The TridentConfig class has the following APIs to work with UI toolkit handlers:

  • addUIToolkitHandler(UIToolkitHandler) – registers the UI toolkit handler
  • removeUIToolkitHandler(UIToolkitHandler) – unregisters the UI toolkit handler
  • getUIToolkitHandlers() – retrieves an unmodifiable collection of all registered (core and custom) UI toolkit handlers

The UIToolkitHandler entries in the plugin descriptor files allow application code to support additional Java-based UI toolkits. The value associated with this key must be the fully qualified class name of an application class that implements the org.pushingpixels.trident.UIToolkitHandler interface.

Respecting the threading rules

The UIToolkitHandler.isHandlerFor(Object) is used to determine whether the main timeline object is a component / widget for the specific UI toolkit. At runtime, all fields registered with the Timeline.addPropertyToInterpolate methods will be changed on the UI thread using the UIToolkitHandler.runOnUIThread method.

In the simple Swing example that interpolates the foreground color of a button on mouse rollover, the timeline is configured as

Timeline rolloverTimeline = new Timeline(button);
rolloverTimeline.addPropertyToInterpolate("foreground", Color.blue,
	Color.red);

If you put a breakpoint in the JComponent.setForeground(Color) – which is called on every timeline pulse – you will see that it is called on the Swing Event Dispatch Thread. Internally, this is what happens:

  • When the timeline is created, all registered UI toolkit handlers are asked whether they are handlers for the specified object
  • The org.pushingpixels.trident.swing.SwingToolkitHandler registered in the core library returns true for the button object in its isHandlerFor(Object)
  • On every timeline pulse, a Runnable object is created internally. The run() method calls the setters for all registered fields – using the PropertyInterpolator.interpolate method of the matching property interpolator
  • This Runnable is passed to the UIToolkitHandler.runOnUIThread method of the matching UI toolkit handler.

And this is how SwingToolkitHandler.runOnUIThread() is implemented:

@Override
public void runOnUIThread(Runnable runnable) {
	if (SwingUtilities.isEventDispatchThread())
		runnable.run();
	else
		SwingUtilities.invokeLater(runnable);
}

Running custom application code on UI thread

The flow described above works for the fields registered with the Timeline.addPropertyToInterpolate methods. What about the custom application callbacks registered with the Timeline.addCallback()? If the callback methods need to respect the UI threading rules of the matching toolkit, the TimelineCallback implementation class needs to be tagged with the org.pushingpixels.trident.callback.RunOnUIThread annotation.

Callback implementations marked with this annotation will have both onTimelineStateChanged and onTimelinePulse invoked on the UI thread, making it safe to query and change the UI. The UIThreadTimelineCallbackAdapter is a core adapter class that is marked with this annotation.

Querying the readiness of the timeline object

The isInReadyState(Object) is the third and final method in the UIToolkitHandler interface. After the specific UI toolkit handler has declared that it will handle the main object of the specific timeline (by returning true from the isHandlerFor(Object) method), it will be used to interpolate the registered fields and run the registered callbacks. However, some UI toolkits may impose additional restrictions on when the UI object is ready to be queried / changed.

For example, once an SWT control is disposed, it will throw an SWTException in the setForeground method. So, if the application code is running a slow animation that changes the foreground color of a button, and the application window containing this button is disposed in the meantime, the call to setForeground should be skipped.

Concluding the series on adding animations to enable rich interactivity expected from modern SWT applications, here is what we have seen so far:

  • Part 1 – adding simple animation behavior to such scenarios as component appearance (fade in) and window disposal (fade-out) using built in and custom class attributes and setters.
  • Part 2 – adding animated load progress indication while the application is loading data.
  • Part 3 – loading the album art matching the specific search string and asynchronously displaying the associated images.
  • Part 4 – scrolling the album covers showed in the container and adding animations to the scrolling.
  • Part 5 – complex transition scenarios.

How can you run this code locally?

  • Get the latest SVN snapshots of Trident and Granite
  • Add the following Eclipse libraries to the project: org.eclipse.core.jobs, org.eclipse.equinox.common, org.eclipse.osgi
  • The Granite distribution contains the lib/amazon.jar. It has been created with the following steps:
    • wsimport -d ./build -s ./src -p com.ECS.client.jax http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl .
    • jar cvf ../amazon.jar .
  • Get an Amazon E-commerce key
  • Run the org.pushingpixels.granite.DemoApp class, passing your Amazon key as the only parameter to this class, adding the Amazon, Trident and Granite classes, as well as Eclipse jars to the classpath

If all went right, you should see the main application running and displaying Sarah McLachlan albums as in this video:

I hope you enjoyed this series. If you’re interested in adding rich animations to your SWT applications, you’re more than welcome to explore Trident and Granite and report any bugs and missing features in the project forums and mailing lists.

After adding such animation effects as fading, load progress, asynchronous load of images and smooth scrolling in the application window connected to the Amazon backend, it’s time to talk about more complex transition scenarios. In this entry i’m going to talk about displaying larger album art and scrollable track listing when the specific album is selected, along with a complex transition between selected albums. 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 detailed view of the selected album (and you can view the videos in the first part of this series):

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

The full sources of this view are in the SVN repository, and i’m going to talk about the full transition scenario that is played when the user selects a specific album. This scenario has six steps:

  1. (Relevant when the details window already shows album art) – collapse the album art component and track listing component to fully overlap.
  2. (In parallel with step 1) – load the new album art from the Internet (based on the URL returned from the original Amazon E-commerce request).
  3. (After steps 1 and 2 have both completed) – set the loaded album art on the album art component. This may also cause resizing the album art if it cannot fully fit in the available space.
  4. (In parallel with step 3) – set the list of album tracks on the track listing component.
  5. (After steps 3 and 4 have both completed) – cross fade the old album art to the new album art.
  6. (After step 5 has been completed) – move the album art component (that displays the new album art) and the track listing to be displayed side by side.

To implement this complex timeline scenario, the code uses the rendezvous timeline scenario provided by Trident. Timeline.RendezvousSequence allows simple branch-and-wait ordering. The rendezvous scenario has a stage-like approach. All actors belonging to the same stage run in parallel, while actors in stage N+1 wait for all actors in stage N to be finished. The RendezvousSequence.rendezvous marks the end of one stage and the beginning of another.

Here is how the code looks like:

   /**
    * Returns the timeline scenario that implements a transition from the
    * currently shown album item (which may be null) to the
    * specified album item.
    *
    * @param albumItem
    *            The new album item to be shown in this window.
    * @return The timeline scenario that implements a transition from the
    *         currently shown album item (which may be null) to
    *         the specified album item.
    */
   private TimelineScenario getShowAlbumDetailsScenario(final Item albumItem) {
      TimelineScenario.RendezvousSequence scenario = new TimelineScenario.RendezvousSequence();

      // step 1 - move album art and track listing to the same location
      Timeline collapseArtAndTracks = new Timeline(this);
      collapseArtAndTracks.addPropertyToInterpolate("overlayPosition",
            this.overlayPosition, 0.0f);
      collapseArtAndTracks.addCallback(new UIThreadTimelineCallbackAdapter() {
         int startingRegionWidth;

         @Override
         public void onTimelineStateChanged(TimelineState oldState,
               TimelineState newState, float durationFraction,
               float timelinePosition) {
            if (newState == TimelineState.READY) {
               startingRegionWidth = (int) (BigAlbumArt.TOTAL_DIM * (1 + overlayPosition));
            }
            updateShellRegion(timelinePosition);
         }

         @Override
         public void onTimelinePulse(float durationFraction,
               float timelinePosition) {
            updateShellRegion(timelinePosition);
         }

         private void updateShellRegion(float timelinePosition) {
            int regionWidth = (int) (startingRegionWidth + timelinePosition
                  * (BigAlbumArt.TOTAL_DIM - startingRegionWidth));
            Region newRegion = new Region();
            newRegion.add(BigAlbumArt.TOTAL_DIM - regionWidth / 2, 0,
                  regionWidth, BigAlbumArt.TOTAL_DIM);
            Shell shell = getShell();
            if (!shell.isDisposed())
               getShell().setRegion(newRegion);
         }
      });
      collapseArtAndTracks.setDuration((int) (500 * this.overlayPosition));
      scenario.addScenarioActor(collapseArtAndTracks);

      // step 2 (in parallel) - load the new album art
      final Image[] albumArtHolder = new Image[1];
      EclipseJobTimelineScenarioActor loadNewAlbumArt = new EclipseJobTimelineScenarioActor(
            "Load album art") {
         @Override
         protected org.eclipse.core.runtime.IStatus run(
               org.eclipse.core.runtime.IProgressMonitor arg0) {
            try {
               URL url = new URL(albumItem.getLargeImage().getURL());
               albumArtHolder[0] = new Image(Display.getDefault(), url
                     .openStream());
               return Status.OK_STATUS;
            } catch (Throwable t) {
               t.printStackTrace();
               return Status.CANCEL_STATUS;
            }
         }
      };
      scenario.addScenarioActor(loadNewAlbumArt);
      scenario.rendezvous();

      // step 3 (wait for steps 1 and 2) - replace album art
      TimelineRunnable replaceAlbumArt = new TimelineRunnable() {
         @Override
         public void run() {
            albumArt.setAlbumArtImage(albumArtHolder[0]);
         }
      };
      scenario.addScenarioActor(replaceAlbumArt);

      // step 4 (in parallel) - replace the track listing
      TimelineRunnable replaceTrackListing = new TimelineRunnable() {
         @Override
         public void run() {
            trackListing.setAlbumItem(albumItem);
         }
      };
      scenario.addScenarioActor(replaceTrackListing);
      scenario.rendezvous();

      // step 5 (wait for steps 3 and 4) - cross fade album art from old to
      // new
      Timeline albumArtCrossfadeTimeline = new Timeline(this.albumArt);
      albumArtCrossfadeTimeline.addPropertyToInterpolate("oldImageAlpha",
            255, 0);
      albumArtCrossfadeTimeline
            .addPropertyToInterpolate("imageAlpha", 0, 255);
      albumArtCrossfadeTimeline.addCallback(new SWTRepaintCallback(
            this.albumArt));
      albumArtCrossfadeTimeline.setDuration(400);

      scenario.addScenarioActor(albumArtCrossfadeTimeline);
      scenario.rendezvous();

      // step 6 (wait for step 5) - move new album art and track listing to
      // be side by side.
      Timeline separateArtAndTracks = new Timeline(this);
      separateArtAndTracks.addPropertyToInterpolate("overlayPosition", 0.0f,
            1.0f);
      separateArtAndTracks.addCallback(new UIThreadTimelineCallbackAdapter() {
         @Override
         public void onTimelinePulse(float durationFraction,
               float timelinePosition) {
            updateShellRegion(timelinePosition);
         }

         @Override
         public void onTimelineStateChanged(TimelineState oldState,
               TimelineState newState, float durationFraction,
               float timelinePosition) {
            updateShellRegion(timelinePosition);
         }

         private void updateShellRegion(float timelinePosition) {
            int regionWidth = (int) (BigAlbumArt.TOTAL_DIM * (1.0 + timelinePosition));
            Region newRegion = new Region();
            newRegion.add(BigAlbumArt.TOTAL_DIM - regionWidth / 2, 0,
                  regionWidth, BigAlbumArt.TOTAL_DIM);
            Shell shell = getShell();
            if (!shell.isDisposed())
               getShell().setRegion(newRegion);
         }
      });
      separateArtAndTracks.setDuration(500);
      scenario.addScenarioActor(separateArtAndTracks);

      return scenario;
   }

This scenario uses the full capabilities offered by the Trident timeline scenarios which allow combining multiple timeline scenario actors in a parallel, sequential or custom order. This code uses the following types of timeline scenario actors:

The only special case not covered by the core Trident APIs is seen in steps 1 and 6. Since SWT does not yet support per-pixel translucency on the shell level (see bug 285026), these steps use Shell.setRegion() API to emulate expanding / collapsing panels. This can also be achieved by changing the size and location of the relevant shell, but i have found changing the region to perform better on my machine. Trident core library does not provide a built-in interpolator for the Region class, and this is why steps 1 and 6 register a custom callback on the relevant timelines.

The rest of the code is pretty straightforward. It defines the components for album art and track listing, as well as the float position of the overlay between them (during the collapse / expand steps):

   /**
    * Component that shows the album art.
    */
   private BigAlbumArt albumArt;

   /**
    * Component that shows the scrollable list of album tracks.
    */
   private TrackListing trackListing;

   /**
    * 0.0f - the album art and track listing are completely overlayed, 1.0f -
    * the album art and track listing are completely separate. Is updated in
    * the {@link #currentShowAlbumDetailsScenario}.
    */
   private float overlayPosition;

When these components are added, we make sure that the album art is displayed on top of the track listing (during the collapse stage). In addition, we install a custom layout manager that respects the current value of the overlayPosition field:

      this.setLayout(new Layout() {
         @Override
         protected void layout(Composite composite, boolean flushCache) {
            int w = composite.getBounds().width;
            int h = composite.getBounds().height;

            // respect the current overlay position to implement the sliding
            // effect in steps 1 and 6 of currentShowAlbumDetailsScenario
            int dim = BigAlbumArt.TOTAL_DIM;
            int dx = (int) (overlayPosition * dim / 2);
            albumArt.setBounds((w - dim) / 2 - dx, (h - dim) / 2, dim, dim);
            trackListing.setBounds((w - dim) / 2 + dx, (h - dim) / 2, dim,
                  dim);
         }

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

The overlayPosition is changed in steps 1 and 6 of the main transition scenario, and the public setter requests the container to layout its contents:

   /**
    * Sets the new overlay position of the album art and track listing. This
    * method will also cause revalidation of the main window content pane.
    *
    * @param overlayPosition
    *            The new overlay position of the album art and track listing.
    */
   public void setOverlayPosition(float overlayPosition) {
      this.overlayPosition = overlayPosition;
      this.layout(new Control[] { albumArt, trackListing });
   }

Finally, the scenario itself is created and played when the mouse listener installed on the album overview component detects a mouse click and calls the setAlbumItem API:

   /**
    * Signals that details of the specified album item should be displayed in
    * this window. Note that this window can already display another album item
    * when this method is called.
    *
    * @param albumItem
    *            New album item to show in this window.
    */
   public void setAlbumItem(Item albumItem) {
      if (this.currentShowAlbumDetailsScenario != null)
         this.currentShowAlbumDetailsScenario.cancel();

      this.currentShowAlbumDetailsScenario = this
            .getShowAlbumDetailsScenario(albumItem);
      this.currentShowAlbumDetailsScenario.play();
   }

The rest of the code in this package is very similar to the code examples showed earlier, including custom painting that respects the alpha values, fading out on dispose, translucent window etc.

The final code sample shows how the album details panel is shown. Here, we use a separate Shell placed alongside the bottom edge of the main application window. This shell will have its region animated to emulate expanding / collapsing panels.

currentlyShownWindow = new Shell(mainWindow, SWT.NO_TRIM
      | SWT.DOUBLE_BUFFERED);
currentlyShownWindow.setLayout(new FillLayout());
// place the details window centered along the bottom edge of the
// main application window
Point mainWindowLoc = mainWindow.getLocation();
Point mainWindowDim = mainWindow.getSize();
int x = mainWindowLoc.x + mainWindowDim.x / 2 - BigAlbumArt.TOTAL_DIM;
int y = mainWindowLoc.y + mainWindowDim.y - BigAlbumArt.TOTAL_DIM / 2;
currentlyShownWindow.setSize(2 * BigAlbumArt.TOTAL_DIM,
      BigAlbumArt.TOTAL_DIM);
Region region = new Region();
region.add(BigAlbumArt.TOTAL_DIM / 2, 0, BigAlbumArt.TOTAL_DIM,
      BigAlbumArt.TOTAL_DIM);
currentlyShownWindow.setRegion(region);
region.dispose();
currentlyShownWindow.setLocation(x, y);

currentlyShownWindow.setAlpha(0);
currentlyShownWindow.setVisible(true);
currentlyShownContentPanel = new DetailsContentPanel(
      currentlyShownWindow);
currentlyShownContentPanel.setAlbumItem(albumItem);
currentlyShownWindow
      .layout(new Control[] { currentlyShownContentPanel });

Timeline showWindow = new Timeline(currentlyShownWindow);
showWindow.addPropertyToInterpolate("alpha", 0, 255);
showWindow.setDuration(500);
showWindow.play();

What happens here?

  • Create a new shell and position it in the required location.
  • Set the region to clip the rightmost and leftmost quarters.
  • Set its alpha to 0 (it will be gradually faded in).
  • Set the album item, initiating the transition scenario described above.
  • Create and play the timeline that fades in this window.

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.