Animation blueprints for SWT – main application window

August 6th, 2009

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.