After adding such animation effects as fading, translucency, 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 Onyx project which aims to provide blueprints for animated Swing 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 {@link #doLayout()} to provide the
	 * seamless scroll animation.
	 * 

	 */
	float leadingPosition;

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.
 */
public Stage2Components() {
	super();
	this.comps = new ArrayList();

	// register the mouse wheel listener for scrolling content
	this.addMouseWheelListener(new MouseWheelListener() {
		@Override
		public void mouseWheelMoved(MouseWheelEvent e) {
			if (e.getWheelRotation() > 0) {
				// next
				scrollToNext();
			} else {
				// previous
				scrollToPrevious();
			}
		}
	});

	// create the key input maps to handle the scrolling
	// with left / right arrows
	InputMap inputMap = new ComponentInputMap(this);
	inputMap.put(KeyStroke.getKeyStroke("RIGHT"), "right");
	inputMap.put(KeyStroke.getKeyStroke("KP_RIGHT"), "right");
	inputMap.put(KeyStroke.getKeyStroke("LEFT"), "left");
	inputMap.put(KeyStroke.getKeyStroke("KP_LEFT"), "left");

	// create the relevant action map
	Action rightAction = new AbstractAction("right") {
		@Override
		public void actionPerformed(ActionEvent e) {
			// next
			scrollToNext();
		}
	};
	Action leftAction = new AbstractAction("left") {
		@Override
		public void actionPerformed(ActionEvent e) {
			// previous
			scrollToPrevious();
		}
	};
	ActionMap actionMap = new ActionMap();
	actionMap.put("right", rightAction);
	actionMap.put("left", leftAction);

	// and register the maps
	this.setInputMap(WHEN_IN_FOCUSED_WINDOW, inputMap);
	this.setActionMap(actionMap);
}

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(albumItem);
	this.comps.add(comp);
	this.add(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++;
		revalidate();
	}
}

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

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

@Override
public synchronized void doLayout() {
	if (comps.size() == 0)
		return;

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

Finally, to prevent the album items painting too close to the main container edges we clip the matching graphics in the paintChildren:

@Override
protected void paintChildren(Graphics g) {
	// clip the graphics context
	g.clipRect(10, 10, getWidth() - 20, getHeight() - 20);
	super.paintChildren(g);
}

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.
	 */
	int 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.
 */
public Stage3AnimatedScrolling() {
	super();
	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 cancels the existing timeline (if necessary), and plays a new one:

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

	// and dynamically create a new one to change the
	// leading position
	this.scrollTimeline = new Timeline(this);
	this.scrollTimeline.addPropertyToInterpolate("leadingPosition",
			this.leadingPosition, this.targetLeadingPosition);
	this.scrollTimeline.setDuration(250);
	this.scrollTimeline.setEase(new Spline(0.7f));

	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 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;
		revalidate();
	}
}

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.

After adding such animation effects as fading, translucency and load progress to the application window while it connects to the Amazon backend, it’s time to talk about displaying the search 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 Onyx project which aims to provide blueprints for animated Swing 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):

Each album item displays the following:

  • Album art image
  • Album title
  • Album price

When the application starts, it sends a query to the Amazon backend and displays the load progress indication while the query is processed. When the query results are returned, the application adds an album overview component for each one of the results (details in the next entry). However, there is one more step that needs to be done.

The query results do not contain the actual images, but rather the URLs pointing at those images. The application sends an additional request for each one of those URLs, but we do not want the user to wait until all those images have been downloaded and scaled (if necessary). Instead, an album overview panel is immediately added to the main container, and a image begins loading. Once the image is loaded, it is scaled and then faded in in the matching component.

Here is a walkthrough for the relevant application class:

public class AlbumOverviewComponent extends JComponent {
	/**
	 * The dimensions of the overview image.
	 */
	public static final int OVERVIEW_IMAGE_DIM = 100;

	/**
	 * The original album art.
	 */
	private BufferedImage image;

	/**
	 * Indicates whether the image loading is done.
	 */
	private boolean imageLoadedDone;

	/**
	 * The alpha value of the image. Is updated in the fade-in timeline which
	 * starts after the image has been successfully loaded and scaled.
	 */
	private float imageAlpha;

These fields store the image itself, the alpha value (while the image is faded-in) and boolean indication that the image is available for painting. The later is needed to mark the end of loading and scaling stage.

We also have fields that store the overall component alpha, as well as the album caption and price:

	/**
	 * The album caption.
	 */
	private String caption;

	/**
	 * The album price.
	 */
	private String price;

	/**
	 * The alpha value of this component. Is updated in the fade-in timeline
	 * which starts when this component becomes a part of the host window
	 * hierarchy.
	 */
	private float alpha;

and a few static fields for the layout purposes:

	/**
	 * Component insets.
	 */
	private static final int INSETS = 7;

	/**
	 * Default width of this component.
	 */
	public static final int DEFAULT_WIDTH = 160;

	/**
	 * Default height of this component.
	 */
	public static final int DEFAULT_HEIGHT = 180;

Here is the constructor that creates a new album overview component from the album description:

/**
 * Creates a new component that shows overview informartion on the specified
 * album.
 *
 * @param albumItem
 *            Information on an album.
 */
public AlbumOverviewComponent(final Item albumItem) {
	this.caption = albumItem.getItemAttributes().getTitle();
	this.price = albumItem.getItemAttributes().getListPrice()
		.getFormattedPrice();
	this.imageLoadedDone = false;
	this.imageAlpha = 0.0f;

	this.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
	this.setOpaque(false);
	this.alpha = 0.0f;

	this.addHierarchyListener(new HierarchyListener() {
		@Override
		public void hierarchyChanged(HierarchyEvent e) {
			Timeline shownTimeline = new Timeline(
				AlbumOverviewComponent.this);
			shownTimeline.addPropertyToInterpolate("alpha", 0.0f, 1.0f);
			shownTimeline.addCallback(new Repaint(
					AlbumOverviewComponent.this));
			shownTimeline.setDuration(1000);
			shownTimeline.play();
		}
	});

	SwingUtilities.invokeLater(new Runnable() {
		@Override
		public void run() {
			getLoadImageScenario(albumItem).play();
		}
	});
}

Here, we:

  • Set the caption and the title from the album item (this class is generated from the Amazon E-commerce WSDL).
  • Set the alphas to zero.
  • Add a listener to fade in the component once it’s added to the window hierarchy (as in the previous entries)
  • Get the timeline scenario that will load, scale and fade in the image and play it – see below.

The most interesting code is in the getLoadImageScenario. It returns a Trident timeline scenario that has the following sequential steps:

  • Load the BufferedImage from the specified URL. This is done with a TimelineSwingWorker.
  • Scale the loaded image to fit the available dimensions. This is done with a TimelineRunnable.
  • Fade in the scaled image. This is done with a Timeline.

Let’s see the code of this method:

/**
 * Returns the timeline scenario that loads, scaled and fades in the
 * associated album art.
 *
 * @param albumItem
 *            Album item.
 * @return The timeline scenario that loads, scaled and fades in the
 *         associated album art.
 */
private TimelineScenario getLoadImageScenario(final Item albumItem) {
	TimelineScenario loadScenario = new TimelineScenario.Sequence();

It creates a sequential timeline scenario. This is a utility that allows specifying a sequence of timelines, swing workers and runnables and have them run one after another – each one waiting until the previously added one has finished.

First up is the wrapped SwingWorker that loads the image:

// load the image
TimelineSwingWorker imageLoadWorker =
		new TimelineSwingWorker() {
	@Override
	protected Void doInBackground() throws Exception {
		URL url = new URL(albumItem.getMediumImage().getURL());
		image = ImageIO.read(url);
		return null;
	}
};
loadScenario.addScenarioActor(imageLoadWorker);

The next one is the image scaler. Note that for very large images it would be better to have this as a SwingWorker as well. However, in this particular case we are using the “medium” album art images which will not be overly big, and we would be needlessly hogging SwingWorkers that are better suited to long image loading operations.

// scale if necessary
TimelineRunnable scaler = new TimelineRunnable() {
	@Override
	public void run() {
		if (image != null) {
			float vFactor = (float) OVERVIEW_IMAGE_DIM
					/ (float) image.getHeight();
			float hFactor = (float) OVERVIEW_IMAGE_DIM
					/ (float) image.getWidth();
			float factor = Math.min(1.0f, Math.min(vFactor, hFactor));
			if (factor < 1.0f) {
				// scaled to fit available area
				image = OnyxUtils.getScaledInstance(image,
					(int) (factor * image.getWidth()),
					(int) (factor * image.getHeight()),
					RenderingHints.VALUE_INTERPOLATION_BICUBIC,
					true);
			}

			imageLoadedDone = true;
		}
	}
};
loadScenario.addScenarioActor(scaler);

Finally, we add a timeline to fade in the scaled image:

// and fade it in
Timeline imageFadeInTimeline = new Timeline(AlbumOverviewComponent.this);
imageFadeInTimeline.addPropertyToInterpolate("imageAlpha", 0.0f, 1.0f);
imageFadeInTimeline
		.addCallback(new Repaint(AlbumOverviewComponent.this));
imageFadeInTimeline.setDuration(500);
loadScenario.addScenarioActor(imageFadeInTimeline);

return loadScenario;

Now we need a couple of public setters for the alpha attributes (so that the main Trident engine can interpolate them):

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

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

And finally, the custom painting:

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

	g2d.setComposite(AlphaComposite.SrcOver.derive(this.alpha));
	g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
			RenderingHints.VALUE_ANTIALIAS_ON);
	g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
			RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

	g2d.setPaint(new GradientPaint(0, 0, new Color(0, 0, 0, 196), 0,
			DEFAULT_HEIGHT, new Color(0, 0, 0, 0)));
	g2d.fillRoundRect(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT, 18, 18);
	g2d.drawRoundRect(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT, 18, 18);

	if (this.imageLoadedDone) {
		Graphics2D g2dImage = (Graphics2D) g2d.create();
		g2dImage.setComposite(AlphaComposite.SrcOver.derive(this.alpha
			* this.imageAlpha));
		// draw the album art image
		g2dImage.drawImage(this.image, (this.getWidth() - this.image
			.getWidth()) / 2, INSETS
			+ (OVERVIEW_IMAGE_DIM - this.image.getHeight()) / 2, null);
		g2dImage.dispose();
	}

	g2d.setColor(Color.white);
	g2d.setFont(UIManager.getFont("Label.font").deriveFont(11.0f));

	FontMetrics fontMetrics = g2d.getFontMetrics();
	int textY = INSETS + OVERVIEW_IMAGE_DIM + fontMetrics.getHeight();
	int textX = INSETS;
	int textWidth = DEFAULT_WIDTH - INSETS - textX;
	OnyxUtils.paintMultilineText(g2d, this.caption, textX, textWidth,
		textY, 2);

	g2d.setColor(new Color(64, 140, 255));
	OnyxUtils.paintMultilineText(g2d, this.price, textX, textWidth, textY
		+ 2 * g2d.getFontMetrics().getAscent(), 1);

	g2d.dispose();
}

A few points about the painting:

  • The current alpha values are used to provide the overall and image fade in effects
  • The image is painted only when the imageLoadedDone is true
  • The OnyxUtils.paintMultilineText is a helper method to paint a multiline text

Here we have seen how to load the album art matching the specific search string and asynchronously display the associated images. The next entry is going to talk about scrolling the album covers showed in the container and how to add animations to the scrolling.

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.