Over the course of this week i’m talking about different concepts in the Trident animation library for Java applications. Part ten talks about the plugin layer allows interested applications to provide additional property interpolators for custom application classes and support additional Java-based UI toolkits.
Plugin overview
The core functionality of the Trident library can be extended to address custom needs of the specific applications. Out of the box Trident supports:
- Interpolating float and integer fields of any Java object that provides the matching public setter methods.
- Swing and SWT UI toolkits, respecting the threading rules and providing interpolators for the custom graphic classes
The extensibility (plugin) layer allows interested applications to:
- Provide additional property interpolators for custom application classes
- Support additional Java-based UI toolkits
Creating a Trident plugin
A Trident plugin is specified by the META-INF/trident-plugin.properties
file that should be placed in the runtime classpath. Note that you can have multiple plugins in the same runtime environment – if each one is coming from a different classpath jar, for example.
The format of trident-plugin.properties
is simple. Each line in this file should be of the following format:
Key=FullyQualifiedClassName
There are two supported keys:
Sample plugin specification
The core Trident library contains a plugin that supports Swing and SWT UI toolkits, as well as property interpolators for a few core classes. Here are the contents of the plugin descriptor:
UIToolkitHandler=org.pushingpixels.trident.swing.SwingToolkitHandler
PropertyInterpolatorSource=org.pushingpixels.trident.swing.AWTPropertyInterpolators
UIToolkitHandler=org.pushingpixels.trident.swt.SWTToolkitHandler
PropertyInterpolatorSource=org.pushingpixels.trident.swt.SWTPropertyInterpolators
PropertyInterpolatorSource=org.pushingpixels.trident.interpolator.CorePropertyInterpolators
Property interpolator plugins
The PropertyInterpolatorSource entries in the plugin descriptor files allow application code to provide property interpolators for custom application classes. The value associated with this key must be the fully qualified class name of an application class that implements the org.pushingpixels.trident.interpolator.PropertyInterpolatorSource
interface.
This interface has one method – public Set<PropertyInterpolator> getPropertyInterpolators()
– which returns a set of custom property interpolators. Custom property interpolators can be used in two ways:
- The
Timeline.addPropertyToInterpolate(String, Object, Object, PropertyInterpolator)
API to explicitly specify the property interpolator to be used
- The
Timeline.addPropertyToInterpolate(String, Object, Object)
API that will choose the property interpolator that matches the types of the from and to values
Property interpolator specification
The org.pushingpixels.trident.interpolator.PropertyInterpolator
interface has two methods.
The public Class getBasePropertyClass()
is used to choose the property interpolator in the Timeline.addPropertyToInterpolate(String, Object, Object)
. Internally, all registered property interpolators are queried to check whether they support the specified from and to values using the Class.isAssignableFrom(Class)
. The first property interpolator that has a match for both values will be used.
For example, the PointInterpolator
in the core AWT property interpolator source (AWTPropertyInterpolators
class) has the following implementation of this method:
@Override
public Class getBasePropertyClass() {
return Point.class;
}
The public T interpolate(T from, T to, float timelinePosition)
is used to compute the interpolated value during the current timeline pulse. For example, the PointInterpolator
in the core AWT property interpolator source (AWTPropertyInterpolators
class) has the following implementation of this method:
public Point interpolate(Point from, Point to, float timelinePosition) {
int x = from.x + (int) (timelinePosition * (to.x - from.x));
int y = from.y + (int) (timelinePosition * (to.y - from.y));
return new Point(x, y);
}
Bringing it together
Let’s look at the following Swing snippet that has a class with a Point
field and a timeline that interpolates the value of that field:
import java.awt.*;
public static class MyRectangle {
private Point corner = new Point(0, 0);
public void setCorner(Point corner) {
this.corner = corner;
}
...
}
Timeline move = new Timeline(rectangle);
move.addPropertyToInterpolate("corner", new Point(0, 0),
new Point(100, 80));
move.playLoop(RepeatBehavior.REVERSE);
What happens when move.addPropertyToInterpolate
is called? Internally, the Trident core looks at all available property interpolators and finds that the AWTPropertyInterpolators.PointInterpolator
is the best match for the passed values (which are both java.awt.Point
s). Then, at every pulse of the move
timeline, the MyRectangle.setCorner(Point)
is called.
Note that the application code did not explicitly specify which property interpolator should be used.
UI toolkit plugins
Graphical applications are a natural fit for animations, and Trident core has built-in support for Swing and SWT. This support covers threading rules, custom property interpolators and repaint timelines.
The UIToolkitHandler entries in the plugin descriptor files allow application code to support additional Java-based UI toolkits. The value associated with this key must be the fully qualified class name of an application class that implements the org.pushingpixels.trident.UIToolkitHandler
interface. Most modern UI toolkits have threading rules that the applications must respect in order to prevent application freeze and visual artifacts. The threading rules for both Swing and SWT specify that the UI-related operations must be done on a special UI thread, and the methods in the UIToolkitHandler
are used to determine the relevance of these threading rules.
Respecting the threading rules
The UIToolkitHandler.isHandlerFor(Object)
is used to determine whether the main timeline object is a component / widget for the specific UI toolkit. At runtime, all fields registered with the Timeline.addPropertyToInterpolate
methods will be changed on the UI thread using the UIToolkitHandler.runOnUIThread
method.
In the simple Swing example that interpolates the foreground color of a button on mouse rollover, the timeline is configured as
Timeline rolloverTimeline = new Timeline(button);
rolloverTimeline.addPropertyToInterpolate("foreground", Color.blue,
Color.red);
If you put a breakpoint in the JComponent.setForeground(Color)
– which is called on every timeline pulse – you will see that it is called on the Swing Event Dispatch Thread. Internally, this is what happens:
- When the timeline is created, all registered UI toolkit handlers are asked whether they are handlers for the specified object
- The
org.pushingpixels.trident.swing.SwingToolkitHandler
registered in the core library returns true
for the button object in its isHandlerFor(Object)
- On every timeline pulse, a
Runnable
object is created internally. The run()
method calls the setters for all registered fields – using the PropertyInterpolator.interpolate
method of the matching property interpolator
- This
Runnable
is passed to the UIToolkitHandler.runOnUIThread
method of the matching UI toolkit handler.
And this is how SwingToolkitHandler.runOnUIThread()
is implemented:
@Override
public void runOnUIThread(Runnable runnable) {
if (SwingUtilities.isEventDispatchThread())
runnable.run();
else
SwingUtilities.invokeLater(runnable);
}
Running custom application code on UI thread
The flow described above works for the fields registered with the Timeline.addPropertyToInterpolate
methods. What about the custom application callbacks registered with the Timeline.addCallback()
? If the callback methods need to respect the UI threading rules of the matching toolkit, the TimelineCallback
implementation class needs to be tagged with the org.pushingpixels.trident.callback.RunOnUIThread
annotation.
Callback implementations marked with this annotation will have both onTimelineStateChanged
and onTimelinePulse
invoked on the UI thread, making it safe to query and change the UI. The UIThreadTimelineCallbackAdapter
is a core adapter class that is marked with this annotation.
Querying the readiness of the timeline object
The isInReadyState(Object)
is the third and final method in the UIToolkitHandler
interface. After the specific UI toolkit handler has declared that it will handle the main object of the specific timeline (by returning true
from the isHandlerFor(Object)
method), it will be used to interpolate the registered fields and run the registered callbacks. However, some UI toolkits may impose additional restrictions on when the UI object is ready to be queried / changed.
For example, once an SWT control is disposed, it will throw an SWTException
in the setForeground
method. So, if the application code is running a slow animation that changes the foreground color of a button, and the application window containing this button is disposed in the meantime, the call to setForeground
should be skipped.
SWT implementation of the UI toolkit handler
The trident-plugin.properties
descriptor bundled with the core Trident library has the following line:
UIToolkitHandler=org.pushingpixels.trident.swt.SWTToolkitHandler
And the SWTToolkitHandler
is:
public class SWTToolkitHandler implements UIToolkitHandler {
@Override
public boolean isHandlerFor(Object mainTimelineObject) {
return (mainTimelineObject instanceof Widget);
}
@Override
public boolean isInReadyState(Object mainTimelineObject) {
return !((Widget) mainTimelineObject).isDisposed();
}
@Override
public void runOnUIThread(Runnable runnable) {
Display.getDefault().asyncExec(runnable);
}
}
This is a very simple implementation of a UI toolkit handler that respects the relevant threading rules:
- The first method implements the logic that associates this handler with all SWT widgets
- The second method marks disposed widgets to skip the property interpolation / callback invocations
- The third method runs the UI related logic on the SWT thread
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 play
ed. 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 eight talks about timeline scenarios that allow creating parallel, sequential, staged or arbitary execution graphs of timelines, runnables, Swing workers and custom application actors.
Timeline scenarios
Timeline scenario allows combining multiple timeline scenario actors in a parallel, sequential or custom order.
There are three core types of timeline scenario actors:
Additional types of timeline scenario actors can be added in the application code by implementing the TimelineScenario.TimelineScenarioActor
interface.
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
Timeline scenario kinds
There are three built-in timeline scenario kinds that address the most common dependencies between the actors:
-
Timeline.Parallel
runs all the actors in a parallel fashion
-
Timeline.Sequence
runs the actors one after another in the order they have been added
-
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.
Simple Swing timeline scenario
The following example shows a Swing application with a simple timeline scenario that launches five parallel timelines. It shows the code behind this video, where every volley is a separate timeline, and all currently playing volleys are part of the same timeline scenario.
In the code, there are three “hierarchy” levels of fireworks:
- The entire fireworks display – this is a timeline scenario that consists of five volley explosions.
- The volley explosion implemented in
VolleyExplosion
class – this is a collection of single explosions that have the same color and originate from the same explosion center point.
- The single explosion implemented in
SingleExplosion
class – this is a fading circle that represents a single “leaf” part of the volley explosion.
The code behind the single explosion is quite simple:
public class SingleExplosion {
float x;
float y;
float radius;
float opacity;
Color color;
public SingleExplosion(Color color, float x, float y, float radius) {
this.color = color;
this.x = x;
this.y = y;
this.radius = radius;
this.opacity = 1.0f;
}
public void setX(float x) {
this.x = x;
}
public void setY(float y) {
this.y = y;
}
public void setRadius(float radius) {
this.radius = radius;
}
public void setOpacity(float opacity) {
this.opacity = opacity;
}
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setComposite(AlphaComposite.SrcOver.derive(this.opacity));
g2d.setColor(this.color);
g2d.fill(new Ellipse2D.Float(this.x - this.radius, this.y
- this.radius, 2 * radius, 2 * radius));
g2d.dispose();
}
}
It has four fields that specify its location, size, opacity and color. Each field except the color has a public setter that is used in the timeline created in the parent volley explosion. Finally, it has a custom painting implementation that paints the graphical representation of the single volley.
The volley explosion is implemented by the following class:
public class VolleyExplosion {
private int x;
private int y;
private Color color;
private Set circles;
public VolleyExplosion(int x, int y, Color color) {
this.x = x;
this.y = y;
this.color = color;
this.circles = new HashSet();
}
public TimelineScenario getExplosionScenario() {
TimelineScenario scenario = new TimelineScenario.Parallel();
int duration = 1000 + (int) (1000 * Math.random());
for (int i = 0; i < 18; i++) {
float dist = (float) (50 + 10 * Math.random());
float radius = (float) (2 + 2 * Math.random());
for (float delta = 0.6f; delta <= 1.0f; delta += 0.2f) {
float circleRadius = radius * delta;
double degrees = 20.0 * (i + Math.random());
float radians = (float) (2.0 * Math.PI * degrees / 360.0);
float initDist = delta * dist / 10.0f;
float finalDist = delta * dist;
float initX = (float) (this.x + initDist
* Math.cos(radians));
float initY = (float) (this.y + initDist
* Math.sin(radians));
float finalX = (float) (this.x + finalDist
* Math.cos(radians));
float finalY = (float) (this.y + finalDist
* Math.sin(radians));
SingleExplosion circle = new SingleExplosion(this.color,
initX, initY, circleRadius);
Timeline timeline = new Timeline(circle);
timeline.addPropertyToInterpolate("x", initX, finalX);
timeline.addPropertyToInterpolate("y", initY, finalY);
timeline.addPropertyToInterpolate("opacity", 1.0f, 0.0f);
timeline.setDuration(duration - 200
+ (int) (400 * Math.random()));
timeline.setEase(new Spline(0.4f));
synchronized (this.circles) {
circles.add(circle);
}
scenario.addScenarioActor(timeline);
}
}
return scenario;
}
public void paint(Graphics g) {
synchronized (this.circles) {
for (SingleExplosion circle : this.circles) {
circle.paint(g);
}
}
}
}
The timeline scenario that implements this volley explosion:
- Each single explosion is implemented as a separate timeline.
- Scenario has random duration
- Single explosions are created at almost evenly distributed angles (every 20 degrees) and at almost evenly distributed distance from the center (three for each angle).
- The scenario has 54 different timelines, one for each single explosion.
Now we get to the main application class. It implements the following functionality:
- Playing five explosion volleys (five timeline scenarios).
- Waiting for all five to be done.
- Playing another five – repeating the previous two steps.
- Listening to the mouse events, suspending the currently playing scenarios on mouse press, and resuming them on mouse release.
The code starts by declaring the relevant data structures:
public class Fireworks extends JFrame {
private Set volleys;
private Map volleyScenarios;
private JPanel mainPanel;
Here is the constructor of this class:
public Fireworks() {
this.mainPanel = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
synchronized (volleys) {
for (VolleyExplosion exp : volleys)
exp.paint(g);
}
}
};
this.mainPanel.setBackground(Color.black);
this.mainPanel.setPreferredSize(new Dimension(480, 320));
Timeline repaint = new SwingRepaintTimeline(this);
repaint.playLoop(RepeatBehavior.LOOP);
this.volleys = new HashSet();
this.volleyScenarios = new HashMap();
this.mainPanel.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
synchronized (volleys) {
for (TimelineScenario scenario : volleyScenarios.values())
scenario.suspend();
}
}
@Override
public void mouseReleased(MouseEvent e) {
synchronized (volleys) {
for (TimelineScenario scenario : volleyScenarios.values())
scenario.resume();
}
}
});
new Thread() {
@Override
public void run() {
while (true) {
if ((mainPanel.getWidth() > 0)
&& (mainPanel.getHeight() > 0)) {
addExplosions(5);
}
}
}
}.start();
this.add(mainPanel);
this.pack();
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
We have:
- A
JPanel
that paints all currently playing volley explosions on black background.
- A looping timeline that repaints the contents of this application.
- The data structures tracking the currently playing explosions.
- The mouse listener that suspends the currently playing scenarios on mouse press and resumes them on mouse release.
- A thread that adds five explosions in an infinite loop (see the explanation of
addExplosions
below)
Here is the method that makes sure that the volley explosions are run in batches of 5, even when they have different durations:
private void addExplosions(int count) {
final CountDownLatch latch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
int r = (int) (255 * Math.random());
int g = (int) (100 + 155 * Math.random());
int b = (int) (50 + 205 * Math.random());
Color color = new Color(r, g, b);
int x = 60 + (int) ((mainPanel.getWidth() - 120) * Math.random());
int y = 60 + (int) ((mainPanel.getHeight() - 120) * Math.random());
final VolleyExplosion exp = new VolleyExplosion(x, y, color);
synchronized (volleys) {
volleys.add(exp);
TimelineScenario scenario = exp.getExplosionScenario();
scenario.addCallback(new TimelineScenarioCallback() {
@Override
public void onTimelineScenarioDone() {
synchronized (volleys) {
volleys.remove(exp);
volleyScenarios.remove(exp);
latch.countDown();
}
}
});
volleyScenarios.put(exp, scenario);
scenario.play();
}
}
try {
latch.await();
} catch (Exception exc) {
}
}
Here, we have:
- A
CountDownLatch
that will be used to wait until all timeline scenarios that run the volley explosions are done
- A random color and a random center location for each one of the volley explosions
- A callback that notifies the count down latch when the timeline scenario is done
- Waiting on the count down latch – until all timeline scenarios are done
And finally, the main method to launch the fireworks:
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new Fireworks().setVisible(true);
}
});
}
Click below for the WebStart demo

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
