After adding such animation effects as fading 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 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):

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 Canvas {
/**
* The dimensions of the overview image.
*/
public static final int OVERVIEW_IMAGE_DIM = 100;
/**
* The original album art.
*/
private Image 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;
/**
* The alpha value of the border. Is updated in the fade-in timeline which
* starts when the mouse moves over this component.
*/
private float borderAlpha;
These fields store the image itself, the alpha value for the image (while the image is faded-in), the alpha value for the border (for the rollover pulsation animation) 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 int 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 information on the specified
* album.
*
* @param albumItem
* Information on an album.
*/
public AlbumOverviewComponent(Composite parent, final Item albumItem) {
super(parent, SWT.DOUBLE_BUFFERED | SWT.TRANSPARENT);
this.caption = albumItem.getItemAttributes().getTitle();
this.price = albumItem.getItemAttributes().getListPrice()
.getFormattedPrice();
this.imageLoadedDone = false;
this.imageAlpha = 0.0f;
this.setCursor(new Cursor(Display.getDefault(), SWT.CURSOR_HAND));
this.alpha = 0;
final Timeline rolloverTimeline = new Timeline(this);
rolloverTimeline.addPropertyToInterpolate("borderAlpha", 0.0f, 0.6f);
rolloverTimeline.addCallback(new SWTRepaintCallback(
AlbumOverviewComponent.this));
rolloverTimeline.setEase(new Spline(0.7f));
rolloverTimeline.setDuration(800);
this.addMouseTrackListener(new MouseTrackAdapter() {
@Override
public void mouseEnter(MouseEvent e) {
rolloverTimeline.playLoop(RepeatBehavior.REVERSE);
}
@Override
public void mouseExit(MouseEvent e) {
rolloverTimeline.playReverse();
}
});
this.addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
if (borderAlpha > 0.0f)
rolloverTimeline.playReverse();
}
});
Timeline shownTimeline = new Timeline(AlbumOverviewComponent.this);
shownTimeline.addPropertyToInterpolate("alpha", 0, 255);
shownTimeline.addCallback(new SWTRepaintCallback(
AlbumOverviewComponent.this));
shownTimeline.setDuration(1000);
shownTimeline.play();
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
getLoadImageScenario(albumItem).play();
}
});
}
This constructor:
- Initializes the album caption and price from the album item (this class is generated from the Amazon E-commerce WSDL)
- Creates a rollover timeline to interpolate the borderAlpha property
- Registers a mouse listener to loop the rollover timeline when the mouse enters this component, and play this timeline in reverse (once) when the mouse exits the component
- Registers a control listener to play the rollover timeline in reverse when the component is resized (gets new bounds)
- Creates and plays a timeline that fades in the component by interpolating the alpha property
- 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 Image from the specified URL. This is done with a custom wrapper around an Eclipse Job.
- 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 Job that loads the image:
// load the image
EclipseJobTimelineScenarioActor imageLoadWorker = new EclipseJobTimelineScenarioActor(
"Load image") {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
URL url = new URL(albumItem.getMediumImage().getURL());
image = new Image(Display.getDefault(), url.openStream());
return Status.OK_STATUS;
} catch (Throwable t) {
t.printStackTrace();
return Status.CANCEL_STATUS;
}
}
};
loadScenario.addScenarioActor(imageLoadWorker);
The next one is the image scaler. Note that for very large images it would be better to have this as an Eclipse Job 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 jobs 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.getImageData().height;
float hFactor = (float) OVERVIEW_IMAGE_DIM
/ (float) image.getImageData().width;
float factor = Math.min(1.0f, Math.min(vFactor, hFactor));
if (factor < 1.0f) {
// scaled to fit available area
image = GraniteUtils.getScaledInstance(image,
(int) (factor * image.getImageData().width),
(int) (factor * image.getImageData().height));
}
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 SWTRepaintCallback(
AlbumOverviewComponent.this));
imageFadeInTimeline.setDuration(500);
loadScenario.addScenarioActor(imageFadeInTimeline);
return loadScenario;
}
Now we need the 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 for the border. Used by the rollover timeline.
*
* @param borderAlpha
* Alpha value for the border.
*/
public void setBorderAlpha(float borderAlpha) {
this.borderAlpha = borderAlpha;
}
/**
* Sets the alpha value. Used by the fade-in timeline.
*
* @param alpha
* Alpha value for this component.
*/
public void setAlpha(int alpha) {
this.alpha = alpha;
}
And finally, the custom painting:
this.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
GC gc = e.gc;
gc.setAlpha(alpha);
gc.setAntialias(SWT.ON);
Pattern pattern = new Pattern(e.display, 0, 0, 0,
DEFAULT_HEIGHT, e.display
.getSystemColor(SWT.COLOR_BLACK), 196,
e.display.getSystemColor(SWT.COLOR_BLACK), 0);
gc.setBackgroundPattern(pattern);
gc.setForegroundPattern(pattern);
gc.fillRoundRectangle(0, 0, DEFAULT_WIDTH - 1,
DEFAULT_HEIGHT - 1, 18, 18);
gc.drawRoundRectangle(0, 0, DEFAULT_WIDTH - 1,
DEFAULT_HEIGHT - 1, 18, 18);
pattern.dispose();
if (borderAlpha > 0.0f) {
// show the pulsating bluish outline of the rollover album
Color borderColor = new Color(e.display, 64, 140, 255);
Pattern borderPattern = new Pattern(e.display, 0, 0, 0,
DEFAULT_HEIGHT, borderColor,
(int) (196 * borderAlpha), borderColor, 0);
LineAttributes currLineAttr = gc.getLineAttributes();
gc.setLineAttributes(new LineAttributes(2.0f,
SWT.CAP_ROUND, SWT.JOIN_ROUND));
gc.setForegroundPattern(borderPattern);
gc.drawRoundRectangle(1, 1, DEFAULT_WIDTH - 2,
DEFAULT_HEIGHT - 2, 18, 18);
gc.setLineAttributes(currLineAttr);
borderPattern.dispose();
borderColor.dispose();
}
if (imageLoadedDone) {
gc.setAlpha((int) (alpha * imageAlpha));
// draw the album art image
gc.drawImage(image, (getBounds().width - image
.getImageData().width) / 2,
INSETS
+ (OVERVIEW_IMAGE_DIM - image
.getImageData().height) / 2);
gc.setAlpha(alpha);
}
FontData fontData = gc.getDevice().getSystemFont()
.getFontData()[0];
gc.setFont(new Font(gc.getDevice(), fontData.getName(), 9,
SWT.NORMAL));
FontMetrics fontMetrics = gc.getFontMetrics();
int textY = INSETS + OVERVIEW_IMAGE_DIM
+ fontMetrics.getDescent();
int textX = INSETS;
int textWidth = DEFAULT_WIDTH - INSETS - textX;
gc
.setForeground(gc.getDevice().getSystemColor(
SWT.COLOR_WHITE));
GraniteUtils.paintMultilineText(AlbumOverviewComponent.this,
gc, caption, textX, textWidth, textY, 2);
gc.setForeground(new Color(gc.getDevice(), 64, 140, 255));
GraniteUtils.paintMultilineText(AlbumOverviewComponent.this,
gc, price, textX, textWidth, textY + 2
* fontMetrics.getHeight(), 1);
}
});
A few points about the painting:
- The current alpha values are used to provide the fade-in effects on the overall component, the loaded image and the pulsating rollover effect on the border
- The image is painted only when the imageLoadedDone is true
- The GraniteUtils.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 SWT applications showed a simple window with an overlapping close button and animated fade-in / fade-out sequences. In this entry i’m going to talk about showing an animated load progress indication while the application is connecting to the Amazon backend. This code is part of the Granite project which aims to provide blueprints for animated SWT applications powered by the Trident animation library.
As a reminder, here is a screenshot of the main skeleton Granite window:

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

While the previous part showed the code for the main window and the close button, it’s now time to look at the main album overview panel. For the demo purposes, the code is built in a layered fashion, with each layer adding more functional and animation behavior.
We start with the base class that provides the functionality of dragging the entire application window with the mouse:
public class Stage0Base extends Canvas {
/**
* Creates the basic container.
*/
public Stage0Base(final Composite composite) {
super(composite, SWT.DOUBLE_BUFFERED);
Listener l = new Listener() {
Point origin;
public void handleEvent(Event e) {
switch (e.type) {
case SWT.MouseDown:
origin = new Point(e.x, e.y);
break;
case SWT.MouseUp:
origin = null;
break;
case SWT.MouseMove:
if (origin != null) {
Shell shell = composite.getShell();
Point p = shell.getDisplay().map(shell, null, e.x, e.y);
shell.setLocation(p.x - origin.x, p.y - origin.y);
}
break;
}
}
};
this.addListener(SWT.MouseDown, l);
this.addListener(SWT.MouseUp, l);
this.addListener(SWT.MouseMove, l);
}
}
The next layer adds the load progress indication. Here is how it looks under full opacity:

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

There are two separate attributes that control the load progress animation. The first controls the alpha, fading the load progress in on load start and fading it out on load end. The second controls the stripes offset and is responsible for creating a continuous indefinite appearance of “marching ants” progress. Each one is controlled by a separate timeline, and here we need to synchronize these two timelines:
- On load start, we start both timelines.
- On load end, we start the fade out timeline, and once it’s done, we cancel the looping “marching ants” timeline.
The progress bar is implemented as a custom SWT control that extends the Canvas class. This control tracks both the position of the progress and the alpha value of the progress bar. Here is the skeleton of the progress bar control, along with the matching attribute declarations:
protected static class ProgressBarIndicator extends Canvas {
/**
* The current position of the {@link #loadingBarLoopTimeline}.
*/
float loadingBarLoopPosition;
/**
* The current alpha value of the loading progress bar. Is updated by
* the {@link #loadingBarFadeTimeline}.
*/
int loadingBarAlpha;
public ProgressBarIndicator(Composite composite) {
super(composite, SWT.TRANSPARENT | SWT.DOUBLE_BUFFERED
| SWT.NO_BACKGROUND);
/**
* Sets the new alpha value of the loading progress bar. Is called by
* the {@link #loadingBarFadeTimeline}.
*
* @param loadingBarAlpha
* The new alpha value of the loading progress bar.
*/
public void setLoadingBarAlpha(int loadingBarAlpha) {
this.loadingBarAlpha = loadingBarAlpha;
}
/**
* Sets the new loop position of the loading progress bar. Is called by
* the {@link #loadingBarLoopTimeline}.
*
* @param loadingBarLoopPosition
* The new loop position of the loading progress bar.
*/
public void setLoadingBarLoopPosition(float loadingBarLoopPosition) {
this.loadingBarLoopPosition = loadingBarLoopPosition;
}
}
The main class declares an instance of the progress bar, as well as the two timelines:
public class Stage1LoadingProgress extends Stage0Base {
/**
* The looping timeline to animate the indefinite load progress. When
* {@link #setLoading(boolean)} is called with true
, this
* timeline is started. When {@link #setLoading(boolean)} is called with
* false
, this timeline is cancelled at the end of the
* {@link #loadingBarFadeTimeline}.
*/
Timeline loadingBarLoopTimeline;
/**
* The timeline for showing and hiding the loading progress bar. When
* {@link #setLoading(boolean)} is called with true
, this
* timeline is started. When {@link #setLoading(boolean)} is called with
* false
, this timeline is started in reverse.
*/
Timeline loadingBarFadeTimeline;
/**
* The pixel width of the load progress visuals.
*/
static final int PROGRESS_WIDTH = 300;
/**
* The pixel height of the load progress visuals.
*/
static final int PROGRESS_HEIGHT = 32;
protected ProgressBarIndicator progressIndicator;
The constructor of the main class defines the custom layout manager that centers the progress bar in its parent, as well as the two timelines:
/**
* Creates a container with support for showing load progress.
*/
public Stage1LoadingProgress(Composite composite) {
super(composite);
this.progressIndicator = new ProgressBarIndicator(this);
this.setLayout(new Layout() {
@Override
protected void layout(Composite composite, boolean flushCache) {
int w = composite.getBounds().width;
int h = composite.getBounds().height;
// put the progress indication in the center
progressIndicator.setBounds((w - PROGRESS_WIDTH) / 2,
(h - PROGRESS_HEIGHT) / 2, PROGRESS_WIDTH,
PROGRESS_HEIGHT);
}
@Override
protected Point computeSize(Composite composite, int wHint,
int hHint, boolean flushCache) {
return new Point(wHint, hHint);
}
});
this.loadingBarLoopTimeline = new Timeline(this.progressIndicator);
this.loadingBarLoopTimeline.addPropertyToInterpolate(
"loadingBarLoopPosition", 0.0f, 1.0f);
this.loadingBarLoopTimeline.addCallback(new SWTRepaintCallback(this));
this.loadingBarLoopTimeline.setDuration(750);
// create the fade timeline
this.loadingBarFadeTimeline = new Timeline(this.progressIndicator);
this.loadingBarFadeTimeline.addPropertyToInterpolate("loadingBarAlpha",
0, 255);
this.loadingBarFadeTimeline
.addCallback(new UIThreadTimelineCallbackAdapter() {
@Override
public void onTimelineStateChanged(TimelineState oldState,
TimelineState newState, float durationFraction,
float timelinePosition) {
if (oldState == TimelineState.PLAYING_REVERSE
&& newState == TimelineState.DONE) {
// after the loading progress is faded out, stop the
// loading animation
loadingBarLoopTimeline.cancel();
progressIndicator.setVisible(false);
}
}
});
this.loadingBarFadeTimeline.setDuration(500);
}
The looping timeline is configured to interpolate the stripe location from zero to one. Later on this timeline will be played in an indefinite loop (cancelled once the load is done), and together with the matching painting code will result in a continuous visual appearance of indefinitely moving stripes. The fade timeline interpolates the alpha value; it also cancels the looping timeline when the state changes from PLAYING_REVERSE to DONE – this signifies the end of the fade out sequence.
Now it’s time for a very simple implementation of load state change:
/**
* Starts or stops the loading progress animation.
*
* @param isLoading
* if true
, this container will display a loading
* progress animation, if false
, the loading
* progress animation will be stopped.
*/
public void setLoading(boolean isLoading) {
if (isLoading) {
this.loadingBarFadeTimeline.play();
this.loadingBarLoopTimeline.playLoop(RepeatBehavior.LOOP);
} else {
this.loadingBarFadeTimeline.playReverse();
}
}
As said before, on load start both timelines start playing (note that the second one is played in a loop). On load end, the fade timeline is played in reverse – once it’s done, it will cancel the second looping timeline in the listener registered in its initialization.
Finally, the painting code respects both the alpha and the looping position – this is done in a custom paint listener registered on the progress bar component:
this.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
if (loadingBarAlpha > 0) {
int width = getBounds().width;
int height = getBounds().height;
GC gc = e.gc;
gc.setAntialias(SWT.ON);
gc.setAlpha(loadingBarAlpha);
Region clipping = new Region(e.display);
gc.getClipping(clipping);
int contourRadius = 8;
// create a round rectangle clip to paint the inner part
// of the progress indicator
Path clipPath = new GraniteUtils.RoundRectangle(
e.display, 0, 0, width, height, contourRadius);
gc.setClipping(clipPath);
Color fill1 = new Color(e.display, 156, 208, 221);
Color fill2 = new Color(e.display, 101, 183, 243);
Pattern pFill1 = new Pattern(e.display, 0, 0, 0,
height / 2.0f, fill1, loadingBarAlpha, fill2,
loadingBarAlpha);
gc.setBackgroundPattern(pFill1);
gc.fillRectangle(0, 0, width, height / 2);
fill1.dispose();
fill2.dispose();
pFill1.dispose();
Color fill3 = new Color(e.display, 67, 169, 241);
Color fill4 = new Color(e.display, 138, 201, 247);
Pattern pFill2 = new Pattern(e.display, 0,
height / 2.0f, 0, height, fill3,
loadingBarAlpha, fill4, loadingBarAlpha);
gc.setBackgroundPattern(pFill2);
gc.fillRectangle(0, height / 2, width, height / 2);
fill3.dispose();
fill4.dispose();
pFill2.dispose();
int stripeCellWidth = 25;
Color stripe1 = new Color(e.display, 36, 155, 239);
Color stripe2 = new Color(e.display, 17, 145, 238);
Pattern pStripe1 = new Pattern(e.display, 0, 0, 0,
height / 2.0f, stripe1, loadingBarAlpha,
stripe2, loadingBarAlpha);
Color stripe3 = new Color(e.display, 15, 56, 200);
Color stripe4 = new Color(e.display, 3, 133, 219);
Pattern pStripe2 = new Pattern(e.display, 0, 0, 0,
height / 2.0f, stripe3, loadingBarAlpha,
stripe4, loadingBarAlpha);
int stripeWidth = 10;
gc.setLineAttributes(new LineAttributes(9.0f));
for (int stripeX = (int) (loadingBarLoopPosition * stripeCellWidth); stripeX < width
+ height; stripeX += stripeCellWidth) {
gc.setBackgroundPattern(pStripe1);
gc.fillPolygon(new int[] {
stripeX - stripeWidth / 2,
0,
stripeX + stripeWidth / 2,
0,
stripeX - stripeCellWidth / 2 + stripeWidth
/ 2,
height / 2,
stripeX - stripeCellWidth / 2 - stripeWidth
/ 2, height / 2 });
gc.setBackgroundPattern(pStripe2);
gc
.fillPolygon(new int[] {
stripeX - stripeCellWidth / 2
- stripeWidth / 2,
height / 2,
stripeX - stripeCellWidth / 2
+ stripeWidth / 2,
height / 2,
stripeX - stripeCellWidth
+ stripeWidth / 2,
height,
stripeX - stripeCellWidth
- stripeWidth / 2, height });
}
stripe1.dispose();
stripe2.dispose();
stripe3.dispose();
stripe4.dispose();
pStripe1.dispose();
pStripe2.dispose();
// restore the original clipping to paint the contour
gc.setClipping(clipping);
clipping.dispose();
gc.setForeground(e.display
.getSystemColor(SWT.COLOR_GRAY));
float lineWeight = 1.6f;
gc.setLineAttributes(new LineAttributes(lineWeight));
Path outline = new GraniteUtils.RoundRectangle(
e.display, lineWeight / 2.0f - 1,
lineWeight / 2.0f - 1, width - 1 - lineWeight
+ 2, height - 1 - lineWeight + 2,
contourRadius - lineWeight / 2);
gc.drawPath(outline);
outline.dispose();
}
}
});
Each load progress stripe is painted as a thick diagonal line, and the X offset of the first stripe is computed based on the current position of the looping timeline. The stripeCellWidth value indicates the horizontal distance between two adjacent stripes, and multiplying it by the current position of the looping timeline results in continuous indefinite progress.
Here we have seen how to add animated load progress indication while the application is loading data. The next entry is going to talk about scrolling the album covers showed in the container and how to add animations to the scrolling.
In this first technical posting on adding animations to SWT applications i’m going to show a simple window with an overlapping close button and animated fade-in / fade-out sequences. 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 of the main skeleton Granite window:

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

Here is a short walkthrough of the main application container:
public class MainContentPanel extends Composite {
AlbumOverviewPanel contentPanel;
CloseButton closeButton;
It is a regular SWT Composite with two components – the content panel and the close button. Let’s take a look at the constructor of this panel:
public MainContentPanel(Shell shell) {
super(shell, SWT.NONE);
this.contentPanel = new AlbumOverviewPanel(this);
this.closeButton = new CloseButton(this);
// move the close button to be painted on top of the content panel
this.closeButton.moveAbove(this.contentPanel);
We add the two components and move the close button to be painted on top of the albums. 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:
this.setLayout(new Layout() {
@Override
protected void layout(Composite composite, boolean flushCache) {
int width = composite.getBounds().width;
int height = composite.getBounds().height;
contentPanel.setBounds(0, 0, width, height);
int closeButtonDim = 35;
closeButton.setBounds(width - closeButtonDim, 0,
closeButtonDim, closeButtonDim);
}
@Override
protected Point computeSize(Composite composite, int wHint,
int hHint, boolean flushCache) {
return new Point(wHint, hHint);
}
});
When the main application shell is created, it is centered on the screen and has alpha set to 0, making it completely transparent (the DemoApp class is a simple wrapper around the MainContentPanel that wires it the Amazon backend):
Display display = new Display();
Shell shell = new Shell(display, SWT.NO_TRIM);
final DemoApp app = new DemoApp(shell, args[0]);
shell.setLayout(new FillLayout());
// center the shell in the display bounds
Rectangle pDisplayBounds = shell.getDisplay().getBounds();
int width = 480;
int height = 200;
shell.setBounds((pDisplayBounds.width - width) / 2,
(pDisplayBounds.height - height) / 2, width, height);
shell.setAlpha(0);
shell.open();
Note the SWT.NO_TRIM flag passed to the Shell constructor – this flag strips the window decorations (border, title pane). Once the shell is opened, we play a short timeline that fades in the main application window using the core SWT Shell.setAlpha API – which gets values between 0 and 255, as compared to Swing’s Window.setOpacity that gets values between 0.0 and 1.0:
Timeline fadeInShellTimeline = new Timeline(shell);
fadeInShellTimeline.addPropertyToInterpolate("alpha", 0, 255);
fadeInShellTimeline.setDuration(500);
fadeInShellTimeline.play();
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 Canvas class with a integer field that stores the current alpha channel:
public class CloseButton extends Canvas {
/**
* 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.
*/
int alpha;
The code uses the foreground attribute of the Control 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(Composite parent) {
super(parent, SWT.TRANSPARENT);
this.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_WHITE));
this.alpha = 0;
It creates the button with SWT.TRANSPARENT flag – equivalent to marking a Swing component 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 a mouse listener to close the ancestor shell when this button is activated:
this.addMouseListener(new MouseAdapter() {
@Override
public void mouseDown(MouseEvent e) {
DetailsWindowManager.disposeCurrentlyShowing();
// fade out the main shell and dispose it when it is
// at full transparency
GraniteUtils.fadeOutAndDispose(getShell(), 500);
}
});
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 Control.setForeground API):
// timeline for the rollover effect (interpolating the
// button's foreground color)
final Timeline rolloverTimeline = new Timeline(this);
rolloverTimeline.addPropertyToInterpolate("foreground", parent
.getDisplay().getSystemColor(SWT.COLOR_WHITE), new Color(parent
.getDisplay(), 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.addMouseTrackListener(new MouseTrackAdapter() {
@Override
public void mouseEnter(MouseEvent e) {
rolloverTimeline.play();
}
@Override
public void mouseExit(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 start a timeline to fade in the button when it first is added to the window hierarchy:
// fade in the component
Timeline shownTimeline = new Timeline(CloseButton.this);
shownTimeline.addPropertyToInterpolate("alpha", 0, 255);
shownTimeline.addCallback(new SWTRepaintCallback(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(int 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 add a paint listener to our button:
this.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
GC gc = e.gc;
gc.setAntialias(SWT.ON);
// use the current alpha
gc.setAlpha(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:
int width = getBounds().width;
int height = getBounds().height;
// paint the background - black fill and a dark outline
// based on the current foreground color
gc
.setBackground(gc.getDevice().getSystemColor(
SWT.COLOR_BLACK));
gc.fillOval(1, 1, width - 3, height - 3);
gc.setLineAttributes(new LineAttributes(2.0f));
Color currFg = getForeground();
int currR = currFg.getRed();
int currG = currFg.getGreen();
int currB = currFg.getBlue();
Color darkerFg = new Color(gc.getDevice(), currR / 2,
currG / 2, currB / 2);
gc.setForeground(darkerFg);
gc.drawOval(1, 1, width - 3, height - 3);
darkerFg.dispose();
// paint the outer cross (always white)
gc
.setForeground(gc.getDevice().getSystemColor(
SWT.COLOR_WHITE));
gc.setLineAttributes(new LineAttributes(6.0f, SWT.CAP_ROUND,
SWT.JOIN_ROUND));
int offset = width / 3;
gc.drawLine(offset, offset, width - offset - 1, height - offset
- 1);
gc.drawLine(width - offset - 1, offset, offset, height - 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 paint listener 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)
gc.setForeground(currFg);
gc.setLineAttributes(new LineAttributes(4.2f, SWT.CAP_ROUND,
SWT.JOIN_ROUND));
gc.drawLine(offset, offset, width - offset - 1, height - offset
- 1);
gc.drawLine(width - offset - 1, offset, offset, height - offset
- 1);
}
});
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.
This entry has shown 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.