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.

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 three talks about APIs to play, loop, resume, suspend and cancel timelines, as well as registering callbacks to listen to timeline life cycle events.

Timeline states

A timeline goes through different timeline states. The Timeline.TimelineState enum lists all possible timeline states, with the basic ones being:

  • Idle for timelines that are not playing. A timeline is idle when it’s been created but not played, or after it has finished playing
  • Playing forward for timelines that interpolate fields from start value to end value.
  • Playing reverse for timelines that interpolate fields from end value to start value.
  • Done for timelines that have finished playing. A done timeline becomes idle after notifying all listeners (see below).

Playing timelines

When the timeline is created, it is in the idle state. An idle timeline can be configured by the application code – even after it has finished playing. The configuration includes adding properties to interpolate, changing the initial delay and duration, adding callbacks and changing the ease function.

To start playing a timeline use the Timeline.play() method. At every pulse the timeline will interpolate all registered properties (using the public setters), as well as notify all registered callbacks. If the timeline is already playing, it will continue playing from the same point.

Some scenarios required playing the timeline in reverse. In the example below we want to animate the button foreground from blue to red when the mouse moves over the button – this is done in the mouseEntered method. To provide consistent UI behavior, we also want to animate the foreground color from red back to blue when the mouse is moved away from the button – this is done by calling the Timeline.playReverse() in the mouseExisted method.

final Timeline rolloverTimeline = new Timeline(button);
rolloverTimeline.addPropertyToInterpolate("foreground", Color.blue,
		Color.red);
button.addMouseListener(new MouseAdapter() {
	@Override
	public void mouseEntered(MouseEvent e) {
		rolloverTimeline.play();
	}

	@Override
	public void mouseExited(MouseEvent e) {
		rolloverTimeline.playReverse();
	}
});

Suppose the user moves the mouse over the button and then quickly moves it away. You do not want to have the reverse play start from the end value (full red color) – this will create a noticeable color flicker on the screen. Internally, playReverse() detects that this timeline is already playing (forward), and starts playing it in reverse from its current position.

Replaying timelines

While play() and playReverse() respect the current timeline position for already playing timelines, some scenarios require restarting the timeline. The Timeline.replay() and Timeline.replayReverse() can be used in these cases. Code example below shows the replay() API used to restart the rollover animation that interpolates the background color of a single grid rectangle from yellow to black color:

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

Looping timelines

While most timelines need to play only once, some application scenarios require running timelines in loop. Pulsating the system tray icon to indicate new messages or showing an indefinite progress while your application connects over a slow line are examples of looping timelines.

Looping timelines are created and configured in exactly the same way as regular timelines, and they can interpolate float and custom properties of the associated main timeline object. The only difference is the way the looping timeline is played. There are two Timeline methods to start playing a looping timeline:

  • Timeline.playLoop(RepeatBehavior)
  • Timeline.playLoop(int, RepeatBehavior)

The first method starts an infinite loop (at least until the timeline is canceled). The second method runs the timeline for the specified number of loops. The Timeline.RepeatBehavior enum specifies what happens when the looping timeline reaches the “end” of the loop. Each timeline loop changes the internal duration fraction which is a number between 0.0 and 1.0. While a regular timeline ends once the fraction reaches the value 1.0, a looping timeline continues. The difference between the repeat behaviors is in the way the timeline fraction is computed:

  • In the loop mode the timeline fraction starts from 0.0, is interpolated to 1.0, and once that value is reached, it is reset it back to 0.0.
  • In the reverse mode, the timeline fraction is interpolated during odd loops from 0.0 to 1.0, and is interpolated during even loops from 1.0 down to 0.0.

As an example, the loop mode can be used for circular indefinite progress indication, where the matching “lead” angle is interpolated between 0 and 360 degrees. The reverse mode can be used for displaying indefinite linear progress indication that oscillates between the left and right markers.

Additional timeline operations

A timeline can be put in the suspended state by calling the Timeline.suspend() method. A suspended timeline can be resumed with the Timeline.resume() method.

To cancel a playing timeline, call the Timeline.cancel() method. 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.

Tracking timeline state

Simple application scenarios create timelines, configure them with fields to interpolate and then play them. However, a more complicated application logic may require tracking the state changes of the timeline. The Timeline.addCallback(TimelineCallback) allows registering a custom callback that will be notified in the following cases:

  • TimelineCallback.onTimelineStateChanged() – this is called whenever the timeline state is changed. For example, calling Timeline.suspend() will notify all the registered listeners that the timeline has changed its state from playing to suspended.
  • TimelineCallback.onTimelinePulse() – this is called on every timeline pulse.

The second method can be used by applications that do not wish to add public setters for all the fields that participate in the timelines. Instead of using the Timeline.addPropertyToInterpolate() to interpolate the fields via public setters, a timeline callback that interpolates the fields directly in the onTimelinePulse() can be used.

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 two talks about APIs to interpolate primitive and non-primitive fields of Java objects.

Interpolation of primitive fields

A timeline allows changing field values of the associated object. For example, in a fade-in animation the timeline will interpolate the value of alpha field from 0.0 to 1.0. There are two steps involved in setting up such timeline.

The first step is to create a Timeline instance passing the main object. The timeline is then configured to interpolate one or more fields of this main object. Let’s see a simple example:

import org.pushingpixels.trident.Timeline;

public class HelloWorld {
	private float value;

	public void setValue(float newValue) {
		System.out.println(this.value + " -- " + newValue);
		this.value = newValue;
	}

	public static void main(String[] args) {
		HelloWorld hello = new HelloWorld();
		Timeline timeline = new Timeline(hello);
		timeline.addPropertyToInterpolate("value", 0.0f, 1.0f);
		timeline.play();

		try {
			Thread.sleep(3000);
		} catch (Exception exc) {
		}
	}
}

Here, the timeline has the associated HelloWorld instance; this timeline is instructed to interpolate the value field of that instance from 0.0 to 1.0 over the duration of the timeline.

There is an important assumption that the application code must honor. Each field added with the addPropertyToInterpolate must have the matching public setter.

A timeline can interpolate multiple fields. In the following example the timeline will change values of three fields at each timeline pulse:

Timeline timeline = new Timeline(circle);
timeline.addPropertyToInterpolate("x", initX, finalX);
timeline.addPropertyToInterpolate("y", initY, finalY);
timeline.addPropertyToInterpolate("opacity", 1.0f, 0.0f);

Interpolating non-primitive fields

In addition to interpolating primitive fields, the Timeline.addPropertyToInterpolate APIs allow the application code to interpolate fields of other types. Each interpolation is using the matching property interpolator that implements the org.pushingpixels.trident.interpolator.PropertyInterpolator interface:

public interface PropertyInterpolator {
	public Class getBasePropertyClass();

	public T interpolate(T from, T to, float timelinePosition);
}

The interpolate method is used at each timeline pulse to compute the interpolated value. To interpolate a field using the specific property interpolator call the following Timeline API:

addPropertyToInterpolate(String, Object, Object, PropertyInterpolator)

The parameters are:

  • The field name.
  • The start value.
  • The end value.
  • The property interpolator.

Note that the application is responsible to make sure that the object passed to the timeline constructor has a public setter accepting the interpolated value returned by the interpolate implementation of this property interpolator.

The PropertyInterpolator.getBasePropertyClass is not used when the application code explicitly passes the property interpolator – and it is safe to return any value (including null) from it. This method is used only during dynamic lookup of custom property interpolators.

Additional interpolation capabilities

The addPropertyToInterpolate API above is used to interpolate a specific field from the given start value to the given end value. Application code can also use the following variations of this API:

  • addPropertyToInterpolate(Object, String, Object, Object) – to interpolate the specified field of an object different from the main timeline object. This object is passed as the first parameter to this method
  • addPropertyToInterpolate(Object, String, Object, Object, PropertyInterpolator) – to interpolate the specified field of an object different from the main timeline object using the specified property interpolator

When the start value is not known at timeline initialization time, the following APIs can be used to interpolate the field from its current value (as known at runtime) to the specified end value:

  • addPropertyToInterpolateTo(String, Object) – to interpolate the specified field of the main timeline object
  • addPropertyToInterpolateTo(Object, String, Object) – to interpolate the specified field of an object different from the main timeline object. This object is passed as the first parameter to this method
  • addPropertyToInterpolateTo(Object, String, Object, PropertyInterpolator) – to interpolate the specified field of an object different from the main timeline object using the specified property interpolator

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 one introduces the “Hello world” example.

Timeline is the most important concept in Trident. Here is a simple example that illustrates the basic timeline terms:

import org.pushingpixels.trident.Timeline;

public class HelloWorld {
	private float value;

	public void setValue(float newValue) {
		System.out.println(this.value + " -- " + newValue);
		this.value = newValue;
	}

	public static void main(String[] args) {
		HelloWorld hello = new HelloWorld();
		Timeline timeline = new Timeline(hello);
		timeline.addPropertyToInterpolate("value", 0.0f, 1.0f);
		timeline.play();

		try {
			Thread.sleep(3000);
		} catch (Exception exc) {
		}
	}
}
  • We import the org.pushingpixels.trident.Timeline class.
  • We define a private float attribute of this class, and provide a public setter for this attribute.
  • We create a new instance of our test class and a timeline associated with that instance.
  • We specify that the value attribute of that instance should be interpolated from 0.of to 1.0f when the timeline is played.
  • Finally, we play the timeline.
  • The Thread.sleep(3000) makes sure that the application waits long enough for the timeline to finish playing

Here is the output of this application:

0.0 -- 0.0
0.0 -- 0.436
0.436 -- 0.514
0.514 -- 0.594
0.594 -- 0.672
0.672 -- 0.75
0.75 -- 0.828
0.828 -- 0.906
0.906 -- 0.984
0.984 -- 1.0
1.0 -- 1.0

Note that the actual numbers depend on the current system load. The setter is called by the timeline at timeline pulses. A timeline pulse is the point where the timeline “wakes up” and changes the values of all registered fields. The values are changed based on how much time has passed since the timeline has started playing.

The three basic timeline concepts illustrated in this sample are:

  • A timeline is associated with an object.
  • A timeline interpolates values of object fields using the public setters of the relevant class.
  • The field values are changed at timeline pulses.