Trident part 7 – parallel timelines in Swing and SWT

June 24th, 2009 | 10 Comments »

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


Related posts:

  1. Repaint timelines in Trident 1.3 Trident aims to be a general-purpose animation library for Java applications. However, most of the...
  2. Trident part 6 – simple Swing / SWT examples Over the course of the next few days i’m going to talk about different concepts...
  3. Trident part 9 – key frames Over the course of this week i’m talking about different concepts in the Trident animation...
  4. Trident part 3 – timeline life cycle Over the course of the next few days i’m going to talk about different concepts...


10 Comments on “Trident part 7 – parallel timelines in Swing and SWT”

  1. 1 Adam Armistead said at 9:06 pm on June 24th, 2009:

    The master timeline you mentioned that repaints the entire grid is not displayed in the example code. Although I did find it in the code on the Trident wiki.

  2. 2 Kirill Grouchnikov said at 10:17 pm on June 24th, 2009:

    Adam,

    Looks like WordPress still has problems with PRE areas. The repaint timeline code should be now visible in this entry.

    Thanks
    Kirill

  3. 3 Nick said at 1:10 am on June 25th, 2009:

    Hi Kirill. Thanks for your response in the previous entry. Now onto this one. I don’t think this is your fault but the webstart application looks awful on linux (Ubuntu 8.04, sun java6 up 14). It is great on windows (I haven’t tried it on the mac but expect it to be great as well).

    It stutters on linux.

    I’ve come across this problem before on linux when using the timingframework and someone posted a solution:

    http://forums.java.net/jive/thread.jspa?threadID=38821&tstart=0

    Indeed, as suggested, moving the mouse makes your webstart appication look good again. But before I never got the second solution to work. Putting “Toolkit.getDefaultToolkit().sync();” at the of paintComponent did nothing for me. I haven’t had chance to try it with your code.

    Have you come across this before or do you have any suggestions?

    Thanks, Nick.

  4. 4 Nick said at 1:36 am on June 25th, 2009:

    Hi Kirill. Sorry if this is a double post, my first one vanished on me.

    The webstart application looks great on windows but awful on Linux (Ubuntu 8.04, SunJava6up14). It jerks about and almost flashes. I don’t think this is your fault though as I ran into this problem when using the timingframework.

    Someone came up with a solution here:

    http://forums.java.net/jive/thread.jspa?threadID=38821&tstart=0

    and sure enough, when I move my mouse about it smooths everything out and looks great. I don’t want to have to constantly move my mouse though and unfortunately I never got the second solution (Toolkit.getDefaultToolkit().sync()) to work.

    Do you have any suggestions on how to solve this issue?

    Thanks, Nick.

  5. 5 Dmitri Trembovetski said at 8:15 am on June 25th, 2009:

    Nice demos. Do they have to be signed?

  6. 6 Kirill Grouchnikov said at 1:47 pm on June 25th, 2009:

    Dmitri,

    As with Substance, i intend to only have one copy of Trident for my WebStart demos. Some of those will need signed privileges, and as such i’m already signing this jar. You only need to accept the signature once and it will be good for Substance and Flamingo demos as well.

    Isn’t this the same for JavaFX demos? I think i haven’t seen a single WebStart JavaFX demo that was not signed.

    Thanks
    Kirill

  7. 7 Dmitri Trembovetski said at 11:00 am on June 26th, 2009:

    > Isn’t this the same for JavaFX demos? I think i haven’t seen a single WebStart JavaFX demo that was not signed.

    It’s not (at least, it doesn’t have to be). It’s just that some demo writers are too “sign” trigger-happy.

    The FX runtime is signed, yes, but the apps don’t have to be.

    In your case I guess some demos (like swt ones) may have to be signed, but I don’t see why pure swing ones have to be (and Trident lib itself either).

  8. 8 Kirill Grouchnikov said at 12:51 pm on June 26th, 2009:

    Dmitri,

    I believe i already stated the reasons. Trident will be used in Substance and Flamingo in the next major releases, and the demos for both are signed (since they access the local disk for demo purpose). As such, Trident will need to be signed – and i’m just addressing the scenario in advance.

    The permission mechanism in WebStart is quite flawed. I don’t see why i need to sign *all* jars in the JNLP when only one of them has code that needs that access. But we have to live with these limitations until Sun decides to do something about them (or not).

    Thanks
    Kirill

  9. 9 Dmitri Trembovetski said at 3:01 pm on July 13th, 2009:

    btw, not to start a language/platform comparison (well, may be), but I think the following shows why domain languages may be better (at least in terms of brevity and clarity) than generic ones in a specific domain. Here’s the Snake example in JavaFX script:

    import javafx.animation.transition.FadeTransition;
    import javafx.scene.Scene;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Rectangle;

    Scene {
    fill: Color.BLACK
    content: for (i in [0..19], j in [0..9])
    Rectangle {
    x: i * 21
    y: j * 21
    width: 20
    height: 20
    opacity: 0
    fill: Color.YELLOW
    onMouseEntered: function(e : MouseEvent) {
    FadeTransition {
    duration: 2.5s node: e.node
    fromValue: 1.0 toValue: 0
    }.play();
    }
    }
    }

    Thanks,
    Dmitri

  10. 10 Kirill Grouchnikov said at 3:37 pm on July 13th, 2009:

    Dmitri,

    What happens during the 2.5 second animation if i move the mouse from the specific square and then quickly move it back over the same square? Will i have two fade transitions changing the alpha, or does the second one implicitly cancel the first one?

    Thanks
    Kirill