October 3rd, 2009

Trident 1.1 – custom property interpolators

Trident animation library for Java applications is nearing release 1.1 (code-named Bogeyman), and it’s time to take a look at the new APIs added in this version. This entry is going to talk about working with custom property interpolators.

Property interpolators

Trident supports interpolation of primitive values – such as integers, floats and point / color / rectangle classes of supported UI toolkits. Application code that needs to interpolate fields of these types does not need to explicitly state how the field value is interpolated between the start / current and end value. For other field types the application code can either register custom property interpolators, or explicitly state the property interpolator to be used for computing the field value.

For both cases the application code needs to provide one or more implementations of the org.pushingpixels.trident.interpolator.PropertyInterpolator interface. This 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:

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);
}

Registering custom property interpolators

TimelinePropertyBuilder.interpolatedWith(PropertyInterpolator) API can be used to explicitly state the property interpolator to be used for the specific property. However, using this API may lead to a lot of boilerplate code in applications that have multiple animations of fields of the same custom type. In such cases it is recommended to register custom property interpolators and have Trident automatically pick up the matching interpolator at runtime. Trident provides two ways to register custom interpolators – customization APIs and plugins.

The TridentConfig class has the following APIs to work with property interpolators:

  • addPropertyInterpolatorSource(PropertyInterpolatorSource) – registers all the property interpolators provided by this source
  • addPropertyInterpolator(PropertyInterpolator) – registers the property interpolator
  • removePropertyInterpolator(PropertyInterpolator) – unregisters the property interpolator
  • getPropertyInterpolators() – retrieves an unmodifiable list of all registered (core and custom) property interpolators
  • getPropertyInterpolator(Object...) – retrieves the first property interpolator that matches all the passed objects, or null if no match is found

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) API that will choose the property interpolator that matches the types of the from and to values
  • Use a timeline property builder and the TimelinePropertyBuilder.interpolatedWith() API. The Timeline.addPropertyToInterpolate(TimelinePropertyBuilder) API will use the specified property interpolator

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.Points). 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. The following snippet illustrates the usage of TridentConfig.addPropertyInterpolator API:

   private Ellipse2D ellipse;
 
   private static class Ellipse2DPropertyInterpolator implements
         PropertyInterpolator {
      public Class getBasePropertyClass() {
         return Ellipse2D.class;
      }
 
      @Override
      public Ellipse2D interpolate(Ellipse2D from, Ellipse2D to,
            float timelinePosition) {
         double x = from.getX() + timelinePosition
               * (to.getX() - from.getX());
         double y = from.getY() + timelinePosition
               * (to.getY() - from.getY());
         double w = from.getWidth() + timelinePosition
               * (to.getWidth() - from.getWidth());
         double h = from.getHeight() + timelinePosition
               * (to.getHeight() - from.getHeight());
         return new Ellipse2D.Double(x, y, w, h);
      }
   }
 
   public void setEllipse(Ellipse2D ellipse) {
      this.ellipse = ellipse;
   }
 
   // code to add property interpolator and configure the timeline
   TridentConfig.getInstance().addPropertyInterpolator(
         new Ellipse2DPropertyInterpolator());
 
   Ellipse2D from = new Ellipse2D.Double(10, 10, 100, 50);
   Ellipse2D to = new Ellipse2D.Double(40, 40, 200, 120);
   this.ellipse = (Ellipse2D) from.clone();
 
   Timeline ellipseTimeline = new Timeline(this);
   ellipseTimeline.addPropertyToInterpolate("ellipse", from, to);
   ellipseTimeline.setEase(new Sine());
   ellipseTimeline.setDuration(2000);
   ellipseTimeline.playLoop(RepeatBehavior.REVERSE);

Here, the registered property interpolator is implicitly used to animate the expanding / shrinking ellipse.