May 22nd, 2010

Onyx reloaded

Exactly a year ago I’ve introduced project Onyx – animation blueprints for Swing using the Trident animation library. Onyx is a small application that loads album information for the specific artist from the Internet and displays it as a scrollable gallery. Here is how Onyx looked a year ago:

And here is how Onyx looks now:

Along with running on Java 6 (with Java 7 weekly binaries for Mac nowhere in sight), Onyx also has received a slight visual facelift for the main window. First, let’s see the code for disposing a window by fading it out:

public static void fadeOutAndDispose(final Window window,
      int fadeOutDuration) {
   Timeline dispose = new Timeline(window);
 
   try {
      Class clazz = Class.forName("com.sun.awt.AWTUtilities");
      final Method opacityMethod = clazz.getDeclaredMethod(
            "setWindowOpacity", Window.class, float.class);
 
      dispose.addPropertyToInterpolate(Timeline. property(
            "opacity").from(1.0f).to(0.0f).setWith(
            new PropertySetter() {
               @Override
               public void set(Object obj, String fieldName,
                     Float value) {
                  try {
                     opacityMethod.invoke(null, obj, value);
                  } catch (Exception exc) {
                     exc.printStackTrace();
                  }
               }
            }));
      dispose.addCallback(new UIThreadTimelineCallbackAdapter() {
         @Override
         public void onTimelineStateChanged(TimelineState oldState,
               TimelineState newState, float durationFraction,
               float timelinePosition) {
            if (newState == TimelineState.DONE) {
               window.dispose();
            }
         }
      });
      dispose.setDuration(fadeOutDuration);
      dispose.play();
   } catch (Exception exc) {
      exc.printStackTrace();
   }
}

With Java 7, you can simply do this:

dispose.addPropertyToInterpolate("opacity", 1.0f, 0.0f);

and let Trident call Window.setOpacity method at every timeline pulse. In Java 6 (starting with 6u10), the AWTUtilities.setWindowOpacity needs to be called on every pulse explicitly.

And here is the code for painting the main content window. First, we store the current clip and enable anti-aliasing:

@Override
protected void paintComponent(Graphics g) {
   Graphics2D g2d = (Graphics2D) g.create();
   Shape clip = g2d.getClip();
 
   g2d.setStroke(new BasicStroke(1.0f));
   g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
         RenderingHints.VALUE_ANTIALIAS_ON);

Next, we compute the outer and inner contours of the panel (the inner contour will be painted with slightly brighter colors as shown in the screenshot above):

   int radius = 16;
 
   Shape contour = new RoundRectangle2D.Double(0, 0, getWidth() - 1,
         getHeight() - 1, radius, radius);
   Shape innerContour = new RoundRectangle2D.Double(1, 1, getWidth() - 3,
         getHeight() - 3, radius - 1, radius - 1);

When the main window is first shown and disposed, a special timeline is played to make the window appear and disappear smoothly. The timeline interpolates the alpha field between 0.0 and 0.9. The main panel has two parts – the bright blue header and the black content. While the content is painted based on the current value of alpha, the header uses less translucency:

   g2d.setComposite(AlphaComposite.SrcOver.derive(1.0f - (float) Math.pow(
         1.0f - alpha, 3.0)));

To paint the header, we first update the current clip to make sure that the painting is only done in the header section. Next, we fill the contour, draw the inner contour and draw the outer contour – all based on the specific colors required by the target design:

   // top part
   g2d.clipRect(0, 0, getWidth(), TITLE_HEIGHT);
   g2d.setPaint(new LinearGradientPaint(0, 0, 0, TITLE_HEIGHT,
         new float[] { 0.0f, 0.49999f, 0.5f, 1.0f }, new Color[] {
               new Color(119, 152, 251), new Color(80, 127, 250),
               new Color(48, 109, 250), new Color(10, 97, 250) }));
   g2d.fill(contour);
   g2d.setPaint(new GradientPaint(0, 0, new Color(151, 179, 253), 0,
         TITLE_HEIGHT, new Color(19, 92, 233)));
   g2d.draw(innerContour);
   g2d.setColor(new Color(11, 61, 200));
   g2d.draw(contour);

Now we restore the clip:

   g2d.setClip(clip);

If we have an artist name to display, we position it in the middle of the title area (using font ascent for proper baseline alignment) and paint the string twice to create a lighting effect:

   if (this.searchString != null) {
      g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
            RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
      g2d.setFont(UIManager.getFont("Label.font").deriveFont(14.0f)
            .deriveFont(Font.BOLD));
      int fa = g2d.getFontMetrics().getAscent();
      int x = (getWidth() - g2d.getFontMetrics().stringWidth(
            this.searchString)) / 2;
      int y = (TITLE_HEIGHT + fa) / 2;
      g2d.setColor(new Color(31, 60, 114));
      g2d.drawString(this.searchString, x, y + 1);
      g2d.setColor(new Color(255, 255, 255));
      g2d.drawString(this.searchString, x, y);
   }

The main content area is painted in the same way as the header. We update the clip, fill the contour, draw the inner contour and draw the outer contour:

   // bottom part
   g2d.setComposite(AlphaComposite.SrcOver.derive(this.alpha));
   g2d.clipRect(0, TITLE_HEIGHT, getWidth(), getHeight() - TITLE_HEIGHT
         + 1);
 
   g2d.setColor(new Color(0, 0, 0));
   g2d.fill(contour);
   g2d.setPaint(new GradientPaint(0, TITLE_HEIGHT, new Color(57, 56, 57),
         0, getHeight() - TITLE_HEIGHT, new Color(50, 48, 50)));
   g2d.draw(innerContour);
   g2d.setPaint(new GradientPaint(0, TITLE_HEIGHT, new Color(13, 11, 15),
         0, getHeight() - TITLE_HEIGHT, new Color(15, 8, 13)));
   g2d.draw(contour);

Finally, we draw a single line separator between the title and main content areas:

   // separator
   g2d.setClip(clip);
   g2d.setColor(new Color(12, 11, 12));
   g2d.drawLine(1, TITLE_HEIGHT, getWidth() - 2, TITLE_HEIGHT);
 
   g2d.dispose();
}

To see the latest Onyx in action, click on the button below to launch the WebStart demo. Use the mouse wheel to scroll between the albums. Click a specific album to show a larger cover and the track list.

If you cannot run the demo, here is a short video of Onyx in action:

April 14th, 2010

Trident 1.2 official release

It is a great pleasure to announce the availability of the final release or version 1.2 of Trident animation library (code-named Cookie Jar). The major milestone for this release is moving Substance 6.0 to use Trident – along with validating the library performance and flexibility to support a wide variety of UI animations. Trident 1.2 has also added a few new APIs to address a few common application requirements:

  • Custom property accessors
  • Refined APIs for stopping timelines

You’re more than welcome to take Trident 1.2 for a ride and see what it can do to add animations to your Swing and SWT applications. To see how Trident is used to drive application animation scenarios, see its use in the following projects:

March 15th, 2010

It’s time for… release candidates

It’s my great pleasure today to announce the availability of release candidates of the following projects:

Substance 6.0 main features include:

  • Support for multi-state animations
  • New look for text-based components
  • Custom component states

Click on the button below to launch a signed WebStart application that shows the available Substance features.

Trident 1.2 is a stabilization release and has a few new APIs.

You’re more than welcome to download the release candidates for Trident, Substance and relevant plugins. If you see any exceptions, visual artifacts or anything else that shouldn’t be there – don’t assume that i know about it. Drop me a line on project forums or mailing lists. The final releases for the libraries are scheduled on April 14.

March 8th, 2010

New in Trident 1.2

The Trident animation library was born in February 2009 out of the internal animation layer used in Substance look-and-feel over the last three years. A year after, it is nearing its third official release which focuses mainly on stabilizing the API and ironing out the existing bugs.

The major milestone for this release is moving Substance 6.0 to use Trident – along with validating the library performance and flexibility to support a wide variety of UI animations. Trident 1.2 has also added a few new APIs to address a few common application requirements.

Custom property accessors

The default mechanism to retrieve and update the interpolated field is to use reflection to look up and call the matching published getter and setter. Application code that does not wish to expose these methods should use the following APIs:

  • TimelinePropertyBuilder.getWith(PropertyGetter)new in 1.2
  • TimelinePropertyBuilder.setWith(PropertySetter)
  • TimelinePropertyBuilder.accessWith(PropertyAccessor)new in 1.2

Where the relevant interfaces are defined in the TimelinePropertyBuilder class as follows:

If the timeline property builder is configured with a custom property setter / accessor, the set() will be called at every timeline pulse passing the object, the name of the field and the new value to set on the field. If the timeline property builder is configured with a custom property getter / accessor, the get() will be called when the current value of the field is needed. For example, when the builder is configured with fromCurrent(), the get() will be called to get the current field value when the timeline starts playing.

The following sample shows usage of a custom property setter:

public class CustomSetter {
   private float value;
 
   public static void main(String[] args) {
      final CustomSetter helloWorld = new CustomSetter();
      Timeline timeline = new Timeline(helloWorld);
      PropertySetter propertySetter = new PropertySetter() {
         @Override
         public void set(Object obj, String fieldName, Float value) {
            SimpleDateFormat sdf = new SimpleDateFormat("ss.SSS");
            float oldValue = helloWorld.value;
            System.out.println(sdf.format(new Date()) + " : " + oldValue
                  + " -> " + value);
            helloWorld.value = value;
         }
      };
      timeline.addPropertyToInterpolate(Timeline. property("value")
            .from(0.0f).to(1.0f).setWith(propertySetter));
      timeline.play();
 
      try {
         Thread.sleep(3000);
      } catch (Exception exc) {
      }
   }
}

Here, the CustomSetter class does not wish to expose the value field via a public setter. Instead, a custom property setter is provided to be called on every timeline pulse. Note that while this sample code does update the matching object field, it is not a strict requirement. Your custom property setter can do anything it wants in the set implementation – update a key-value map, update multiple fields or even ignore some of the values altogether.

The following sample shows usage of a custom property accessor backed up by a key-value map:

public class CustomAccessor {
   private Map values = new HashMap();
 
   public static void main(String[] args) {
      final CustomAccessor helloWorld = new CustomAccessor();
      Timeline timeline = new Timeline(helloWorld);
 
      PropertyAccessor propertyAccessor = new PropertyAccessor() {
         @Override
         public Float get(Object obj, String fieldName) {
            return helloWorld.values.get("value");
         }
 
         @Override
         public void set(Object obj, String fieldName, Float value) {
            SimpleDateFormat sdf = new SimpleDateFormat("ss.SSS");
            float oldValue = helloWorld.values.get("value");
            System.out.println(sdf.format(new Date()) + " : " + oldValue
                  + " -> " + value);
            helloWorld.values.put("value", value);
         }
      };
      helloWorld.values.put("value", 50f);
 
      timeline.addPropertyToInterpolate(Timeline. property("value")
            .fromCurrent().to(100.0f).accessWith(propertyAccessor));
      timeline.setDuration(300);
      timeline.play();
 
      try {
         Thread.sleep(1000);
      } catch (Exception exc) {
      }
   }
}

Note that both examples assume that the setter / accessor is used only for one specific field (“value”). You can use the same getter / setter / accessor implementation to access multiple fields – using the fieldName parameter passed to the get() and set() methods.

Stopping a timeline

In some cases you will want to stop a running timeline. There are three different APIs that you can use, each with its own semantics:

  • Timeline.abort(). The timeline transitions to the idle state. No application callbacks or field interpolations are done.
  • Timeline.end()new in 1.2. The timeline transitions to the done state, with the timeline position set to 0.0 or 1.0 – based on the direction of the timeline. After application callbacks and field interpolations are done on the done state, the timeline transitions to the idle state. Application callbacks and field interpolations are done on this state as well.
  • Timeline.cancel(). The timeline transitions to the cancelled state, preserving its current timeline position. After application callbacks and field interpolations are done on the cancelled state, the timeline transitions to the idle state. Application callbacks and field interpolations are done on this state as well.

In addition, there is a method to indicate that a looping timeline should stop once it reaches the end of the loop. For example, suppose that you have a pulsating animation of system tray icon to indicate unread messages. Once the message is read, this animation is canceled in the application code. However, immediate cancellation of the pulsating animation may result in jarring visuals, especially if it is done at the “peak” of the pulsation cycle. Calling Timeline.cancelAtCycleBreak() method will indicate that the looping animation should stop once it reaches the end of the loop.