After adding such animation effects as fading, translucency, 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 Onyx project which aims to provide blueprints for animated Swing 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):

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:
- (Relevant when the details window already shows album art) – collapse the album art component and track listing component to fully overlap.
- (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).
- (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.
- (In parallel with step 3) – set the list of album tracks on the track listing component.
- (After steps 3 and 4 have both completed) – cross fade the old album art to the new album art.
- (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.setDuration((int) (500 * this.overlayPosition));
scenario.addScenarioActor(collapseArtAndTracks);
// step 2 (in parallel) - load the new album art
final BufferedImage[] albumArtHolder = new BufferedImage[1];
TimelineSwingWorker loadNewAlbumArt = new TimelineSwingWorker() {
@Override
protected Void doInBackground() throws Exception {
URL url = new URL(albumItem.getLargeImage().getURL());
albumArtHolder[0] = ImageIO.read(url);
return null;
}
};
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() {
trackListingScroller.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",
1.0f, 0.0f);
albumArtCrossfadeTimeline.addPropertyToInterpolate("imageAlpha", 0.0f,
1.0f);
albumArtCrossfadeTimeline.addCallback(new Repaint(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.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. There are three core types of timeline scenario actors, all used in this code:
- Timelines
-
TimelineSwingWorker
s – extension of SwingWorker
-
TimelineRunnable
– extension of Runnable
To create a custom timeline scenario, use the following APIs of the TimelineScenario
class:
-
public void addScenarioActor(TimelineScenarioActor actor)
adds the specified actor
-
public void addDependency(TimelineScenarioActor actor, TimelineScenarioActor... waitFor)
specifies the dependencies between the actors
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 TrackListingScroller trackListingScroller;
/**
* 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:
Container contentPane = this.getContentPane();
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 w = parent.getWidth();
int h = parent.getHeight();
// 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);
trackListingScroller.setBounds((w - dim) / 2 + dx,
(h - dim) / 2 + 2, dim, dim - 4);
}
});
contentPane.add(albumArt);
contentPane.add(trackListingScroller);
contentPane.setComponentZOrder(trackListingScroller, 1);
contentPane.setComponentZOrder(albumArt, 0);
The overlayPosition
is changed in steps 1 and 6 of the main transition scenario, and the public setter revalidates the container causing the layout:
/**
* 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.getContentPane().invalidate();
this.getContentPane().validate();
}
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();
}
Note how we first cancel the currently playing scenario – this handles quick subsequent selections by the user, reversing the currently playing scenario in the middle.
One last thing to note in the transition scenario:
// 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",
1.0f, 0.0f);
albumArtCrossfadeTimeline.addPropertyToInterpolate("imageAlpha", 0.0f,
1.0f);
albumArtCrossfadeTimeline.addCallback(new Repaint(this.albumArt));
albumArtCrossfadeTimeline.setDuration(400);
Note that this timeline is created on the child album art component. After the new album art has been loaded and scaled (in step 3), we initiate the cross-fading timeline on another object – which is fully supported by Trident timelines.
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 translucent Window placed alongside the bottom edge of the main application window:
currentlyShownWindow = new DetailsWindow();
// place the details window centered along the bottom edge of the
// main application window
Point mainWindowLoc = mainWindow.getLocation();
Dimension mainWindowDim = mainWindow.getSize();
int x = mainWindowLoc.x + mainWindowDim.width / 2
- currentlyShownWindow.getWidth() / 2;
int y = mainWindowLoc.y + mainWindowDim.height
- currentlyShownWindow.getHeight() / 2;
currentlyShownWindow.setLocation(x, y);
currentlyShownWindow.setOpacity(0.0f);
currentlyShownWindow.setBackground(new Color(0, 0, 0, 0));
currentlyShownWindow.setVisible(true);
currentlyShownWindow.setAlbumItem(albumItem);
Timeline showWindow = new Timeline(currentlyShownWindow);
showWindow.addPropertyToInterpolate("opacity", 0.0f, 1.0f);
showWindow.setDuration(500);
showWindow.play();
What happens here?
- Create a new window and position it in the required location.
- Set its opacity to 0.0 (it will be gradually faded in).
- Set its background to a fully transparent color – allowing the collapse / expand stage to show the underlying window.
- Sets the album item, initiating the transition scenario described above.
- Creates and plays the timeline that fades in this window.
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.