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.

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 five talks about supporting Java based UI toolkits, respecting threading rules, custom property interpolators and repaint timelines.

Animations in UI toolkits

Smooth transitions and subdued animations are integral part of many modern graphical applications, and Trident comes with built-in support for Java based UI toolkits. The three UI specific requirements are addressed by the core Trident library:

  • Automatically respecting the threading rules of the UI toolkits
  • Providing property interpolators for classes that represent graphical objects of the UI toolkits
  • Repainting application windows that have continuous animations

Out of the box, Trident supports Swing and SWT. In addition, Trident has a pluggable layer that allows interested applications and third part developers to support additional Java UI toolkits (such as Pivot, Qt Jambi and others).

UI threading rules

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 the special UI thread.

The strictness of the rules is different between the toolkits. For example, Swing allows calling Component.repaint() off the UI thread – internally the repaint request is scheduled to run on that thread. However, SWT is much stricter – repaints (as well as changing any UI property of widget objects) must be done on the UI thread. Failure to do so results in an SWTException being thrown by the UI toolkit.

The core Trident library provides a pluggable behavior to automatically detect animations running on UI components and change the interpolated properties on the toolkit UI thread. In addition, custom application callbacks can be marked to be executed on the that thread.

UI property interpolators

Each UI toolkit has its own set of classes that represent visual objects or properties of graphical objects. Classes such as Color, Point and Rectangle are specific to the UI toolkit. In Swing, these are found in the java.awt package, while in SWT they are located in the org.eclipse.swt.graphics package.

Applications that wish to interpolate properties of these types require the matching property interpolators. The core Trident library provides built-in property interpolators for both AWT / Swing and SWT graphical classes.

Updating the screen

An update to a property of a graphical object should usually be reflected in the visual representation of that object on the screen. Simple operations – such as changing the foreground color of a UI control – automatically repaint the affected control. However, more complicated application animations affect multiple visual objects many times a second. Such scenarios require periodical update of the screen to reflect all the changes that happened in the application objects since the last repaint.

The core Trident library provides special repaint timelines that are usually run in a repeating loop, repainting the contents of the entire window or specific UI component / container.

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 four talks about timeline progress, duration fractions and timeline positions.

Timeline duration

By default, a Trident timeline runs for 500 milliseconds. To change the default timeline duration use the Timeline.setDuration(long) API, passing the required duration in milliseconds. At runtime, the timeline interpolates all registered properties and notifies all registered listeners. Note that while the number of timeline pulses is directly proportional to the timeline duration, the actual number of pulses, as well as the intervals between each successive pair of pulses depends on the current load of the system and the virtual machine. As such, the application code must not make any assumptions about when the timeline pulses will happen, and how many pulses will happen throughout the duration of the timeline.

The Timeline.setInitialDelay(long) method specifies the number of milliseconds the timeline should wait after the application code to play() before starting firing the timeline pulses. For a timeline with no initial delay, the following events are fired:

  • idle -> ready immediately after call to Timeline.play()
  • ready -> playing forward immediately afterwards

For a timeline with non-zero delay, the following events are fired:

  • idle -> ready immediately after call to Timeline.play()
  • ready -> playing forward after the specified initial delay has passed

Timeline position

Each timeline pulse has two associated fractional values – duration fraction and timeline position. Duration fraction is a number between 0.0 and 1.0 that indicates the absolute percentage of the full timeline duration that has passed. For example, in a timeline that lasts 500 ms, a timeline pulse happening 200 ms after the timeline has begun has the associated duration fraction of 0.4.

However, some application scenarios require non-linear rate of change for recreating realistic animations of the real physical world. For example, if your application timeline is interpolating the Y location of a falling ball, strict linear interpolation will result in overly artificial movement. When objects in the real world move, they don’t move at constant speed. For example, a car starts from zero velocity, accelerates to a constant speed, maintains it throughout the main part of the movement and then decelerates to zero velocity as it reaches its final location.

The timeline position is a fractional number between 0.0 and 1.0 that indicates how far the timeline has progressed based on the current ease function. The ease function takes the linearly increasing duration fraction and translates it to the matching timeline position. The Timeline.setEase(TimelineEase) method allows setting a custom ease function on the timeline, where TimelineEase interface has the following method:

public float map(float durationFraction)

The org.pushingpixels.trident.ease package has a number of core ease functions. To illustrate the difference between the different ease functions, we will use the core Spline ease function. The following screenshot shows the mapping between duration fraction and timeline position under Spline with factor of 0.4:

Here, the timeline position has almost linear rate of change throughout the entire duration of the timeline, with little acceleration in the beginning, and little deceleration at the end. Here is the mapping between duration fraction and timeline position under Spline with factor of 0.8:

Here, the acceleration phase is longer, and the rate of change between the acceleration and deceleration phases is higher. As you can see, you can simulate different physical processes using different factors of Spline ease function. Application code can create custom implementation of the TimelineEase interface as well.

Putting it all together

Interpolation of field values for fields registered for the specific timeline is done based on the timeline position and not duration fraction. Application callbacks registered with the Timeline.addCallback method get both values in the TimelineCallback.onTimelinePulse method. This provides the application logic with the information how much time has passed since the timeline has started, as well as how far along the timeline is based on its ease method.