Over the course of this week i’m talking about different concepts in the Trident animation library for Java applications. Part nine talks about key frames that allow interpolating properties between more than two values.
Key frames
The sample application in multiple timelines example has the following basic timeline:
this.rolloverTimeline = new Timeline(this);
this.rolloverTimeline.addPropertyToInterpolate("backgroundColor",
Color.yellow, Color.black);
This creates a timeline that animates the backgroundColor field from yellow to black when the timeline is played. What if you want to animate this field from yellow to green, and then to black? One option is to create two timelines, one to animate from yellow to green, and another to animate from green to black. Since the second timeline needs to wait until the first one ends, you will have to either use the timeline callbacks, or create a parallel timeline scenario.
However, there is a simpler solution for interpolating the value of the specific field between more than two values – key frames. A key frame defines the value of the field at the particular timeline duration fraction (not timeline position).
Key times and key values
In our example, the timeline to interpolate the background color will have three key frames:
- The beginning key frame at key time
0.0f with associated key value of Color.yellow
- The intermediate key frame at the matching key time (any value between
0.0f and 1.0f based on the application requirement) with associated key value of Color.green
- The ending key frame at key time
1.0f with associated key value of Color.black
Each key frame has two mandatory associated properties: key time and key value. Key times must form a strictly increasing sequence that starts at 0.0f and ends at 1.0f. Key values must have either an explicit or an implicit property interpolator. This interpolator must be able to compute interpolated value for any successive pair of key values; this value should be of a class that can be passed to the public setter of the relevant property.
Simple example
To put it all together, here is the definition of a key frame-driven timeline for the example above:
this.rolloverTimeline = new Timeline(this);
KeyValues alphaValues = KeyValues.create(Color.yellow,
Color.green, Color.black);
KeyTimes alphaTimes = new KeyTimes(0.0f, 0.5f, 1.0f);
this.rolloverTimeline.addPropertyToInterpolate("backgroundColor",
new KeyFrames(alphaValues, alphaTimes));
Here, we specify that the backgroundColor starts at yellow, goes to green at half the duration of the timeline, and then goes to black at the end.
Simple key frame example
The SWT application discussed below implements a simple infinite progress indication illustrated in this screenshot:

The bluish highlighter moves from left to right, fading in as it appears on the left edge, and fading out as it disappears on the right edge. There are two properties that control the appearance of the highlighter:
- xPosition – an integer property that is linearly interpolated between the left X and the right X.
- alpha – a float property that starts at
0.0f, goes to 1.0f at 30% of the timeline duration, stays at 1.0f until the timeline reaches its 70% mark and then goes back to 0.0f
The alpha property in this example is interpolated using key frames.
The progress indication panel is an SWT Canvas with two fields and matching public setters:
public static class ProgressPanel extends Canvas {
private int xPosition;
private float alpha;
public void setXPosition(int position) {
xPosition = position;
}
public void setAlpha(float alpha) {
this.alpha = alpha;
}
}
The constructor of this panel wires a mouse listener that starts the indefinite progress animation. The boolean started field tracks whether this animation has been started:
private boolean started;
public ProgressPanel(Composite parent) {
super(parent, SWT.DOUBLE_BUFFERED);
this.xPosition = 0;
this.alpha = 0;
this.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
if (started)
return;
start();
started = true;
}
});
}
The start() method creates a timeline that interpolates the X position and alpha of the progress highlight. The X position is a simple interpolation between two values (taking into account that the highlight should not be painted outside the track). The alpha interpolation uses key frames:
public void start() {
Timeline progressTimeline = new Timeline(this);
int startX = (this.getBounds().width - INNER_WIDTH) / 2 + 18
+ HIGHLIGHTER_WIDTH / 2;
int endX = (this.getBounds().width + INNER_WIDTH) / 2 - 18
- HIGHLIGHTER_WIDTH / 2;
progressTimeline
.addPropertyToInterpolate("xPosition", startX, endX);
KeyValues alphaValues = KeyValues.create(0.0f, 1.0f, 1.0f, 0.0f);
KeyTimes alphaTimes = new KeyTimes(0.0f, 0.3f, 0.7f, 1.0f);
progressTimeline.addPropertyToInterpolate("alpha", new KeyFrames(
alphaValues, alphaTimes));
progressTimeline.setDuration(1500);
progressTimeline.playLoop(RepeatBehavior.LOOP);
}
The panel constructor also creates a repaint timeline so that the progress animation is properly reflected on the screen:
new SWTRepaintTimeline(this).playLoop(RepeatBehavior.LOOP);
The actual painting is done in a custom PaintListener added in the panel constructor. The full code can be found in the test.swt.ProgressIndication class. It uses the matching SWT graphics operations to paint the overall background, the inner gradient background and contour, the track and the track highlight. The track highlight painting uses the current values of both xPosition and alpha fields to display the correct visuals.
Click below for the WebStart demo of the Swing version

Over the course of this week i’m talking about different concepts in the Trident animation library for Java applications. Part seven shows a more complex Swing / SWT example that illustrates usage of multiple timelines running in parallel and affecting different visual areas of the same application window.
Multiple timelines in Swing applications
Trident supports running multiple independent timelines at the same time. This page shows the Swing application behind this video, where every cell rollover is implemented as a separate timeline.
We start with a class that implements a specific grid cell:
public static class SnakePanelRectangle {
private Color backgroundColor;
private boolean isRollover;
private Timeline rolloverTimeline;
public SnakePanelRectangle() {
this.backgroundColor = Color.black;
this.isRollover = false;
this.rolloverTimeline = new Timeline(this);
this.rolloverTimeline.addPropertyToInterpolate("backgroundColor",
Color.yellow, Color.black);
this.rolloverTimeline.setDuration(2500);
}
public void setRollover(boolean isRollover) {
if (this.isRollover == isRollover)
return;
this.isRollover = isRollover;
if (this.isRollover) {
this.rolloverTimeline.replay();
}
}
public void setBackgroundColor(Color backgroundColor) {
this.backgroundColor = backgroundColor;
}
public Color getBackgroundColor() {
return backgroundColor;
}
}
A few major points in this class:
- The default background color of a cell is black.
- The rollover timeline interpolates the background color from yellow to black over a period of 2.5 seconds.
- The rollover timeline is replayed when
setRollover is called with true. This restarts the timeline to interpolate the foreground color from yellow.
The next class implements a cell grid, tracing the mouse events and dispatching the rollover events to the relevant cells:
private static class SnakePanel extends JPanel {
private SnakePanelRectangle[][] grid;
private int ROWS = 10;
private int COLUMNS = 20;
private int DIM = 20;
public SnakePanel() {
this.grid = new SnakePanelRectangle[COLUMNS][ROWS];
for (int i = 0; i < COLUMNS; i++) {
for (int j = 0; j < ROWS; j++) {
this.grid[i][j] = new SnakePanelRectangle();
}
}
this.setPreferredSize(new Dimension(COLUMNS * (DIM + 1), ROWS
* (DIM + 1)));
Timeline repaint = new SwingRepaintTimeline(this);
repaint.playLoop(RepeatBehavior.LOOP);
this.addMouseMotionListener(new MouseMotionAdapter() {
int rowOld = -1;
int colOld = -1;
@Override
public void mouseMoved(MouseEvent e) {
int x = e.getX();
int y = e.getY();
int column = x / (DIM + 1);
int row = y / (DIM + 1);
if ((column != colOld) || (row != rowOld)) {
if ((colOld >= 0) && (rowOld >= 0))
grid[colOld][rowOld].setRollover(false);
grid[column][row].setRollover(true);
}
colOld = column;
rowOld = row;
}
});
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.black);
g2d.fillRect(0, 0, getWidth(), getHeight());
for (int i = 0; i < COLUMNS; i++) {
for (int j = 0; j < ROWS; j++) {
SnakePanelRectangle rect = this.grid[i][j];
Color backgr = rect.getBackgroundColor();
if (!Color.black.equals(backgr)) {
g2d.setColor(backgr);
g2d.fillRect(i * (DIM + 1), j * (DIM + 1), DIM, DIM);
}
}
}
g2d.dispose();
}
}
A few major points in this class:
- A special type of timeline is created and played in a loop. In this example, each cell rollover timeline changes the background color of that cell, but does not cause the repaint. Instead, we have a "master" repaint timeline that runs in a loop and causes the repaint of the entire grid panel.
- The mouse motion listener tracks the mouse location, calling the
setRollover method on relevant cells. Since each cell rollover timeline runs for 2.5 seconds, quick mouse moves will result in multiple timelines running in parallel.
- The painting of each cell respects the current background color of that cell.
Finally, the main method that creates a host frame and adds the cell grid panel to it:
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("Snake");
frame.add(new SnakePanel());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
});
}
Multiple timelines in SWT applications
The matching SWT code is quite similar. The single grid cell:
public static class SnakePanelRectangle {
private Color backgroundColor;
private boolean isRollover;
private Timeline rolloverTimeline;
public SnakePanelRectangle() {
this.backgroundColor = Display.getDefault().getSystemColor(
SWT.COLOR_BLACK);
this.isRollover = false;
this.rolloverTimeline = new Timeline(this);
this.rolloverTimeline.addPropertyToInterpolate("backgroundColor",
Display.getDefault().getSystemColor(SWT.COLOR_YELLOW),
Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
this.rolloverTimeline.setDuration(2500);
}
public void setRollover(boolean isRollover) {
if (this.isRollover == isRollover)
return;
this.isRollover = isRollover;
if (this.isRollover) {
this.rolloverTimeline.replay();
}
}
public void setBackgroundColor(Color backgroundColor) {
this.backgroundColor = backgroundColor;
}
public Color getBackgroundColor() {
return backgroundColor;
}
}
The cell grid:
private static class SnakePanel extends Canvas {
private SnakePanelRectangle[][] grid;
private int ROWS = 10;
private int COLUMNS = 20;
private int DIM = 20;
public SnakePanel(Composite parent) {
super(parent, SWT.DOUBLE_BUFFERED);
this.grid = new SnakePanelRectangle[COLUMNS][ROWS];
for (int i = 0; i < COLUMNS; i++) {
for (int j = 0; j < ROWS; j++) {
this.grid[i][j] = new SnakePanelRectangle();
}
}
Timeline repaint = new SWTRepaintTimeline(this);
repaint.playLoop(RepeatBehavior.LOOP);
this.addMouseMoveListener(new MouseMoveListener() {
int rowOld = -1;
int colOld = -1;
@Override
public void mouseMove(MouseEvent e) {
int x = e.x;
int y = e.y;
int column = x / (DIM + 1);
int row = y / (DIM + 1);
if ((column >= COLUMNS) || (row >= ROWS))
return;
if ((column != colOld) || (row != rowOld)) {
if ((colOld >= 0) && (rowOld >= 0))
grid[colOld][rowOld].setRollover(false);
grid[column][row].setRollover(true);
}
colOld = column;
rowOld = row;
}
});
this.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
GC gc = e.gc;
gc.setBackground(e.display.getSystemColor(SWT.COLOR_BLACK));
gc.fillRectangle(e.x, e.y, e.width, e.height);
for (int i = 0; i < COLUMNS; i++) {
for (int j = 0; j < ROWS; j++) {
SnakePanelRectangle rect = grid[i][j];
Color backgr = rect.getBackgroundColor();
gc.setBackground(backgr);
gc.fillRectangle(i * (DIM + 1), j * (DIM + 1), DIM,
DIM);
}
}
}
});
}
}
and the main method:
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setSize(430, 240);
shell.setText("SWT Snake");
FillLayout layout = new FillLayout();
shell.setLayout(layout);
SnakePanel snake = new SnakePanel(shell);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}
display.dispose();
}
In this two examples we have multiple timeline running in parallel. The main repaint timeline continuously repaints the grid, and each cell has its own rollover timeline. If you move the mouse quickly over the grid, you can end up with dozens of timelines, each updating its own cell - with the "master" repaint timeline looking at the current cell color during the painting.
Click below for the WebStart demo of the Swing version

Over the course of the next few days i’m going to talk about different concepts in the Trident animation library for Java applications. Part six shows simple Swing / SWT examples that highlight Trident support for Java-based UI toolkits.
Simple Swing example
The following example shows how to smoothly change the foreground color of a Swing button on mouse rollover.
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.*;
import org.pushingpixels.trident.Timeline;
public class ButtonFg extends JFrame {
public ButtonFg() {
JButton button = new JButton("sample");
button.setForeground(Color.blue);
this.setLayout(new FlowLayout());
this.add(button);
final Timeline rolloverTimeline = new Timeline(button);
rolloverTimeline.addPropertyToInterpolate("foreground", Color.blue,
Color.red);
rolloverTimeline.setDuration(2500);
button.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
rolloverTimeline.play();
}
@Override
public void mouseExited(MouseEvent e) {
rolloverTimeline.playReverse();
}
});
this.setSize(400, 200);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new ButtonFg().setVisible(true);
}
});
}
}
Here, we have a timeline that interpolates the foreground color between blue and red. The mouse listener registered on the button plays this timeline on mouse enter, and plays this timeline in reverse on mouse exit.
This example shows how the JComponent.setForeground(Color) method is used together with the built in property interpolator for the java.awt.Color class to run the timeline that interpolates the foreground color of a Swing button. Note that since the JComponent.setForeground(Color) also repaints the button, there is no need to explicitly repaint it on every timeline pulse.
If you debug this application and put a breakpoint in the JComponent.setForeground(Color) method, you will see that it is called on the Event Dispatch Thread. This is a built-in capability of the Trident core. It recognizes that the timeline is associated with a Swing component, and calls the setter method (during the timeline pulses) on the EDT.
Simple SWT example
The following example is the SWT version of changing the control foreground color on mouse rollover:
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackAdapter;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.*;
import org.pushingpixels.trident.Timeline;
public class ButtonFg {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setSize(300, 200);
GridLayout layout = new GridLayout();
shell.setLayout(layout);
Button button = new Button(shell, SWT.RADIO);
GridData gridData = new GridData(GridData.CENTER, GridData.CENTER,
true, false);
button.setLayoutData(gridData);
button.setText("sample");
Color blue = display.getSystemColor(SWT.COLOR_BLUE);
Color red = display.getSystemColor(SWT.COLOR_RED);
button.setForeground(blue);
final Timeline rolloverTimeline = new Timeline(button);
rolloverTimeline.addPropertyToInterpolate("foreground", blue, red);
rolloverTimeline.setDuration(2500);
button.addMouseTrackListener(new MouseTrackAdapter() {
@Override
public void mouseEnter(MouseEvent e) {
rolloverTimeline.play();
}
@Override
public void mouseExit(MouseEvent e) {
rolloverTimeline.playReverse();
}
});
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}
display.dispose();
}
}
As with Swing, the Control.setForeground(Color) method is used together with the built in property interpolator for the org.eclipse.swt.graphics.Color class to run the timeline that interpolates the foreground color of an SWT radio button. Note that since the Control.setForeground(Color) also repaints the button, there is no need to explicitly repaint it on every timeline pulse.
If you debug this application and put a breakpoint in the Control.setForeground(Color) method, you will see that it is called on the SWT Thread. This is a built-in capability of the Trident core. It recognizes that the timeline is associated with a SWT component, and calls the setter method (during the timeline pulses) on the SWT thread.
Finally, since both examples are using the Timeline.play() and Timeline.playReverse() methods, the interpolation can be reversed in the middle if the user moves the mouse quickly. The rollover timeline in our example takes 2.5 seconds to complete. Suppose the user moves the mouse over the button, and then after one second moves the mouse away. The call to playReverse detects that this very timeline is already playing, and starts playing it in reverse from its current position.
Release 5.2 of Substance look-and-feel made a few visual changes to the Raven Graphite skins, but these did not address the overall usability of these skins – especially the contrast between the background and the controls, and the background / foreground contrast of text components.
The latest drop of version 5.3dev (code named Reykjavik) features significant overhaul of both Raven Graphite skins, aiming to address the contrast usability issues raised by the users.
Here is a screenshot of a sample application under the Raven Graphite skin in the latest stable 5.2 release:

And here is the same application under the 5.3dev drop:

Here is another screenshot of the same application under the old Raven Graphite visuals:

and the new visuals under the latest 5.3dev drop:

The main changes are:
- Removing the watermark that contributed significant visual noise
- Darker border color for controls, bringing more delineation to check boxes and radio buttons
- Darker background color for text components, resulting in better readability
The same changes were made for the Raven Graphite Glass skin. Here is the sample application under the stable 5.2 release:

and here is the same application under the latest 5.3dev drop:

In addition to the visual changes above, the Raven Graphite Glass skin has removed the glass arc gradient from the toolbars and added a two-tone separator to delineate the title bar / menu bar from the rest of the application content.
To illustrate the visual difference in a larger content, here is a screenshot of a big UI under the stable 5.2 release (click to see the full-size view):

and the same application under the 5.3dev branch:

If you want to take the new visuals for a spin, click on the WebStart button below and change the skin to Raven Graphite and Raven Graphite Glass from the “Skins” menu:

You’re more than welcome to take the latest 5.3dev drop for a spin and leave your comments.