Home > Articles > Programming > Java

Using Transitions to Simplify JavaFX Animations

JavaFX simplifies common animations (such as fades and rotations) by providing 'canned' animation transition classes. Jeff Friesen introduces each of these classes and shows how to create your own custom transition classes.
Like this article? We recommend

JavaFX supports animation via the timeline (one or more key frames that are processed sequentially), key frames (sets of key value variables containing node properties whose values are interpolated along a timeline), key values (properties, their end values, and interpolators), and interpolators (objects that calculate intermediate property values).

Although flexible, this keyframe animation model would normally require you to create the same (or nearly the same) animation boilerplate to perform fades, rotations, and other commonly occurring transitions. Fortunately, JavaFX provides a set of "canned" animated transition classes in the javafx.animation package that save you the bother.

This article takes you on a tour of javafx.animation's transition classes. You first learn about the basic fade, fill, path, pause, rotate, scale, stroke, and translate transition classes, and then learn about the parallel and sequential compound transition classes. Finally, I show you how to create your own transition classes.

Basic Transitions

The javafx.animation package provides classes for performing eight basic transitions:

  • Animate a node's opacity
  • Animate a node's fill color
  • Move a node along a path
  • Do nothing for a while and then perform an action
  • Animate a node's rotation
  • Animate a node's stroke color
  • Animate a node's scaling
  • Animate a node's translation

Fade Transition

FadeTransition fades a node by transitioning its node property's opacity property from a starting value to an ending value over the duration specified by its duration property. The starting and ending values are specified via the following FadeTransition properties and the current value of node's opacity property:

  • fromValue provides the transition's starting value and wraps a double. When not specified, the node's current opacity value is used instead.
  • toValue provides the transition's ending value and wraps a double. When not specified, the starting value plus the value of byValue, which wraps a double, is used instead. When toValue and byValue are specified, toValue takes precedence.

I've created a FadeTDemo application that demonstrates FadeTransition. This application uses this class to animate an image's opacity from opaque to transparent and back three times in 12 seconds when you click the image. Listing 1 presents the source code.

Listing 1—FadeTDemo.java.

import javafx.animation.FadeTransition;

import javafx.application.Application;

import javafx.scene.Group;
import javafx.scene.Scene;

import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

import javafx.stage.Stage;

import javafx.util.Duration;

public class FadeTDemo extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      ImageView iv = new ImageView();
      Image image = new Image("file:res/flowers.jpg");
      iv.setImage(image);

      FadeTransition ft = new FadeTransition();
      ft.setNode(iv);
      ft.setDuration(new Duration(2000));
      ft.setFromValue(1.0);
      ft.setToValue(0.0);
      ft.setCycleCount(6);
      ft.setAutoReverse(true);

      iv.setOnMouseClicked(me -> ft.play());

      Group root = new Group();
      root.getChildren().add(iv);
      Scene scene = new Scene(root, image.getWidth(), image.getHeight());

      primaryStage.setTitle("FadeTransition Demo");
      primaryStage.setScene(scene);
      primaryStage.show();
   }
}

The start() method first creates a javafx.scene.image.ImageView node for displaying an image, loads the image file from the current directory's res subdirectory, and assigns it to this node. Moving forward, it instantiates FadeTransition and configures the object by invoking the following methods:

  • void setNode(Node value) is invoked to set the image view node as the target node.
  • void setDuration(Duration value) is invoked to set the duration of the fade. The fade will complete in exactly 2,000 milliseconds (two seconds).
  • void setFromValue(double value) is invoked to set the starting value for the node's opacity. The starting value is set to 1.0 to indicate a fully opaque image.
  • void setToValue(double value) is invoked to set the ending value for the node's opacity. The ending value is set to 0.0 to indicate a fully transparent image.
  • void setCycleCount(int value) is invoked to set the number of cycles in the fade transition. The number of cycles is set to 6 (three fade-outs and three fade-ins).
  • void setAutoReverse(boolean value) is invoked to specify whether the animation reverses on alternate cycles. The true argument indicates reversal.

Next, a mouse-clicked listener is installed to start the fade transition in response to the image being clicked. This is followed by boilerplate for constructing the scenegraph and adding it to a javafx.scene.Scene (which is sized to the image dimensions), for setting the stage's title and Scene, and for showing the stage with its scene.

Compile Listing 1 (javac FadeTDemo.java) and run the application (java FadeTDemo). You should observe the scene in Figure 1, which is fading from opaque to transparent (at which point you will observe the default white background).

Figure 1

Figure 1 A garden of flowers is fading out. (The image is courtesy of Lydia Jacobs at Public Domain Pictures.)

Fill Transition

FillTransition changes a shape property's fill property from a starting color value to an ending color value over the duration specified by its duration property. The starting and ending color values are specified via the following FillTransition properties and the current value of shape's fill property:

  • fromValue provides the transition's starting color value and wraps a javafx.scene.paint.Color. When not specified, the shape's fill color value is used instead.
  • toValue provides the transition's ending color value and wraps a Color.

I've created a FillTDemo application that demonstrates FillTransition. This application uses this class to animate a circle's fill color repeatedly from yellow to gold and then back to yellow in four seconds. Listing 2 presents the source code, which is very similar to StrokeTDemo's source code (presented later in the article).

Listing 2—FillTDemo.java.

import javafx.animation.FillTransition;
import javafx.animation.Timeline;

import javafx.application.Application;

import javafx.scene.Group;
import javafx.scene.Scene;

import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

import javafx.scene.shape.Circle;

import javafx.stage.Stage;

import javafx.util.Duration;

public class FillTDemo extends Application
{
   final static int SCENE_WIDTH = 300;
   final static int SCENE_HEIGHT = 300;

   @Override
   public void start(Stage primaryStage)
   {
      Stop[] stops = new Stop[] 
      { 
         new Stop(0, Color.BLUE), 
         new Stop(1, Color.DARKBLUE)
      };
      LinearGradient lg = new LinearGradient(0, 0, 0, 1, true,
                                             CycleMethod.NO_CYCLE,
                                             stops);

      Group root = new Group();
      Scene scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT, lg);

      Circle circle = new Circle();
      circle.centerXProperty().bind(scene.widthProperty().divide(2));
      circle.centerYProperty().bind(scene.heightProperty().divide(2));
      circle.setRadius(100);
      circle.setFill(Color.YELLOW);

      root.getChildren().add(circle);

      FillTransition ft = new FillTransition();
      ft.setShape(circle);
      ft.setDuration(new Duration(2000));
      ft.setToValue(Color.GOLD);
      ft.setCycleCount(Timeline.INDEFINITE);
      ft.setAutoReverse(true);
      ft.play();

      primaryStage.setTitle("FillTransition Demo");
      primaryStage.setScene(scene);
      primaryStage.show();
   }
}

The start() method creates a linear gradient (from blue to dark blue) for the scene's background, creates a javafx.scene.Group container for the scenegraph, and creates the scene. Next, it creates a javafx.scene.shape.Circle shape node, binds its center point to the scene's center point so that the circle will always be centered, establishes a radius, and sets the circle's initial fill color.

After adding the circle to the group, start() instantiates FillTransition and configures this object by invoking methods that are similar to FadeTransition's methods except for void setShape(Shape value), which sets the circle as the target shape node—there is no setNode() method.

Compile Listing 2 (javac FillTDemo.java) and run the application (java FillTDemo). You should observe the scene in Figure 2, which consists of a circle whose yellow interior transitions to gold and back to yellow.

Figure 2

Figure 2 The circle's fill color repeatedly transitions between yellow and gold.

Path Transition

PathTransition moves a node along a geometric path by updating its node property's translateX and translateY properties over the duration specified by its duration property. When its orientation property is set to OrientationType.ORTHOGONAL_TO_TANGENT, the path transition regularly updates the node's rotate property.

I've created a PathTDemo application that demonstrates PathTransition. This application uses this class to simulate a car moving over a road. In the simulation, the car is centered over the road's divider line—not a safe practice in the real world. Listing 3 presents the source code.

Listing 3—PathTDemo.java.

import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.PathTransition;
import javafx.animation.PathTransition.OrientationType;
import javafx.animation.Timeline;

import javafx.application.Application;

import javafx.scene.Group;
import javafx.scene.Scene;

import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

import javafx.scene.paint.Color;

import javafx.scene.shape.ArcTo;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;

import javafx.stage.Stage;

import javafx.util.Duration;

public class PathTDemo extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      ImageView car = new ImageView();
      car.setImage(new Image("file:res/car.gif"));
      car.setX(-car.getImage().getWidth() / 2);
      car.setY(300 - car.getImage().getHeight());
      car.setRotate(90);

      PathElement[] path = 
      {
         new MoveTo(0, 300),
         new ArcTo(100, 100, 0, 100, 400, false, false),
         new LineTo(300, 400),
         new ArcTo(100, 100, 0, 400, 300, false, false),
         new LineTo(400, 100),
         new ArcTo(100, 100, 0, 300, 0, false, false),
         new LineTo(100, 0),
         new ArcTo(100, 100, 0, 0, 100, false, false),
         new LineTo(0, 300),
         new ClosePath()
      };

      Path road = new Path();
      road.setStroke(Color.BLACK);
      road.setStrokeWidth(75);
      road.getElements().addAll(path);

      Path divider = new Path();
      divider.setStroke(Color.WHITE);
      divider.setStrokeWidth(4);
      divider.getStrokeDashArray().addAll(10.0, 10.0);
      divider.getElements().addAll(path);

      PathTransition anim = new PathTransition();
      anim.setNode(car);
      anim.setPath(road);
      anim.setOrientation(OrientationType.ORTHOGONAL_TO_TANGENT);
      anim.setInterpolator(Interpolator.LINEAR);
      anim.setDuration(new Duration(6000));
      anim.setCycleCount(Timeline.INDEFINITE);

      Group root = new Group();
      root.getChildren().addAll(road, divider, car);
      root.setTranslateX(50);
      root.setTranslateY(50);
      root.setOnMouseClicked(me -> 
                  {
                    Animation.Status status = anim.getStatus();
                    if (status == Animation.Status.RUNNING &&
                        status != Animation.Status.PAUSED)
                        anim.pause();
                    else
                        anim.play();
                  });
      Scene scene = new Scene(root, 500, 500, Color.DARKGREEN);

      primaryStage.setTitle("PathTransition Demo");
      primaryStage.setScene(scene);
      primaryStage.show();
   }
}

The start() method first creates an ImageView node that's initialized to the image of a car. This node is given an appropriate starting position on the road, which happens to be centered over the divider line. Also, it's rotated 90 degrees to display the car image in a downward vertical position rather than in its default horizontal and right-facing position.

Moving on, a path sequence is constructed to describe the road and the divider line—they both share the same path instructions; the only difference between the two is that the divider line has a narrower stroke width. The sequence describes a square with rounded corners, and having a (0, 0) origin (upper-left corner of the stage). The final javafx.scene.shape.ClosePath object is needed to close the path properly.

start() next creates the road and divider line nodes, breaking the divider line into visible and transparent segments of equal length by initializing its strokeDashArray observable list. The divider line will appear exactly in the center of the road because the road is essentially just a "fat" divider line, and they both share the same coordinates.

At this point, PathTransition is instantiated, and subsequently configured by invoking various transition methods. Some methods behave identically to those described earlier in the context of FadeTransition—note that the Timeline.INDEFINITE argument passed to setCycleCount() indicates an unlimited number of animation cycles. Additionally, the following methods are demonstrated:

  • void setPath(Shape value) is invoked to assign the road shape node to the path property.
  • void setOrientation(PathTransition.OrientationType value) is invoked to specify the node's upright orientation along the path. It looks more natural to keep the car perpendicular to its path.
  • void setInterpolator(Interpolator value) is invoked to specify a linear interpolator, which results in a smooth animation.

The scene is specified as a group of road, divider, and car nodes; with the road being displayed first, the divider being displayed over the road (by virtue of the divider appearing after the road in Group's content sequence), and the car (which is last in this sequence) being displayed over the road and the divider. The Group's translation members position this scene so that it's centered in its window.

Additionally, Group is assigned a mouse handler for playing or pausing the animation. To animate the car, simply click the mouse anywhere over the road, divider, or car. Click the mouse again to pause the animation; a third click restarts the animation, and so on.

Compile Listing 3 (javac PathTDemo.java) and run the application (java PathTDemo). You should observe the scene in Figure 3, which reveals the car (about to turn a corner) as it travels down the road.

Figure 3

Figure 3 The center of the node being animated (the car) serves as the animation's anchor point—it follows the path closely.

Pause Transition

PauseTransition waits for its duration property's value to expire and then executes the event handler assigned to its onFinished property. This transition is useful for inserting pauses between the transitions contained in a sequential transition (discussed later). It's also useful in the context of an image-oriented screensaver, which Listing 4 describes.

Listing 4—PauseTDemo.java.

import javafx.animation.PauseTransition;

import javafx.application.Application;

import javafx.scene.Group;
import javafx.scene.Scene;

import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

import javafx.scene.paint.Color;

import javafx.stage.Stage;

import javafx.util.Duration;

public class PauseTDemo extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      String[] imageNames =
      {
         "elephant.jpg",
         "giraffe.jpg",
         "monkey1.jpg",
         "monkey2.jpg",
         "tiger.jpg"
      };
      ImageView[] iv = new ImageView[imageNames.length];
      for (int i = 0; i < imageNames.length; i++)
         iv[i] = new ImageView(new Image("file:res/" + imageNames[i]));

      Group root = new Group();
      Scene scene = new Scene(root, 0, 0, Color.BLACK);

      PauseTransition pt = new PauseTransition();
      pt.setDuration(new Duration(6000));
      pt.setOnFinished(e ->
                       {
                          int index = (int) rnd(imageNames.length);
                          root.getChildren().clear();
                          iv[index].setX(rnd((scene.getWidth() - 
                                             iv[index].getImage().getWidth())));
                          iv[index].setY(rnd((scene.getHeight() - 
                                             iv[index].getImage().getHeight())));
                          root.getChildren().add(iv[index]);
                          pt.play();
                       });

      primaryStage.setTitle("PauseTransition Demo");
      primaryStage.setScene(scene);
      primaryStage.show();
      primaryStage.setFullScreen(true);

      pt.play();

   }

   public double rnd(double limit)
   {
      return Math.random() * limit;
   }
}

The start() method first creates an array of ImageView nodes, where each node is populated with a javafx.scene.image.Image object whose image is loaded from a file in the res subdirectory of the current directory. (I've chosen images of zoo animals from Public Domain Pictures for this application.)

Next, start() creates an empty group container, which will be populated later in the application. The group is passed as the root of the scenegraph to a Scene constructor to establish the scene. I've chosen to have the scene calculate an initial size by passing 0 for its width and height. I've also selected a black background.

At this point, start() instantiates PauseTransition and assigns it a duration of six seconds. It then invokes PauseTransition's void setOnFinished(EventHandler<ActionEvent> value) method to install an event handler that's invoked when the pause transition's duration expires.

The handler first randomly selects the array index for the next ImageView node to display. It then clears out the previous ImageView node (if any) from the group. Next, the node's upper-left corner is randomly selected so that the image is completely visible on the screen. Finally, the node is added to the group and the pause transition is replayed.

All that remains is to configure and display the stage, set full-screen exclusive mode by invoking javafx.stage.Stage's void setFullScreen(boolean value) method with true as its argument, and play the pause transition. When full-screen exclusive mode is entered, you observe the message Press ESC to exit full-screen mode.

Compile Listing 4 (javac PauseTDemo.java) and run the application (java PauseTDemo). You should observe the scene in Figure 4, which shows the screensaver not in full-screen exclusive mode.

Figure 4

Figure 4 This little fella wants another banana!

Rotate Transition

RotateTransition rotates a node around its center by transitioning its node property's rotate property from a starting value to an ending value (both in degrees) over the duration specified by its duration property. The starting and ending values are specified via the following RotateTransition properties and the current value of node's rotate property:

  • fromAngle provides the transition's starting-angle value and wraps a double. When not specified, the node's current rotate value is used instead.
  • toAngle provides the transition's ending-angle value and wraps a double. When not specified, the starting value plus the value of byAngle, which wraps a double, is used instead. When toAngle and byAngle are specified, toAngle takes precedence.

I've created a RotateTDemo application that demonstrates RotateTransition. This application uses this class to rotate a "single line of text" node continually around its center, with each rotation having a three-second duration. Listing 5 presents the source code.

Listing 5—RotateTDemo.java.

import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.RotateTransition;
import javafx.animation.Timeline;

import javafx.application.Application;

import javafx.geometry.VPos;

import javafx.scene.Group;
import javafx.scene.Scene;

import javafx.scene.effect.Reflection;

import javafx.scene.paint.Color;

import javafx.scene.text.Font;
import javafx.scene.text.Text;

import javafx.stage.Stage;

import javafx.util.Duration;

public class RotateTDemo extends Application
{
   final static int SCENE_WIDTH = 300;
   final static int SCENE_HEIGHT = 300;

   @Override
   public void start(Stage primaryStage)
   {
      Text text = new Text();
      text.setText("JavaFX RotateTransition Demo");
      text.setFont(new Font("Arial Bold", 16));
      text.setTextOrigin(VPos.TOP);
      text.setX(SCENE_WIDTH / 2);
      text.setY(SCENE_HEIGHT / 2);
      text.setTranslateX(-text.layoutBoundsProperty().get().getWidth() / 2);
      text.setTranslateY(-text.layoutBoundsProperty().get().getHeight() / 2);
      text.setEffect(new Reflection());

      RotateTransition rt = new RotateTransition();
      rt.setNode(text);
      rt.setFromAngle(0);
      rt.setToAngle(360);
      rt.setInterpolator(Interpolator.LINEAR);
      rt.setCycleCount(Timeline.INDEFINITE);
      rt.setDuration(new Duration(3000));

      Group root = new Group();
      root.getChildren().add(text);
      Scene scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT, Color.ORANGE);
      scene.setOnMouseClicked(me ->
                              {
                                 Animation.Status status = rt.getStatus();
                                 if (status == Animation.Status.RUNNING &&
                                     status != Animation.Status.PAUSED)
                                    rt.pause();
                                 else
                                    rt.play();
                              });

      primaryStage.setTitle("RotateTransition Demo");
      primaryStage.setScene(scene);
      primaryStage.show();
      primaryStage.setResizable(false);
   }
}

The start() method first creates a javafx.scene.text.Text node that displays a single line of text. This text is positioned with its left edge at the scene's center, and subsequently translated to the left by half its width and height, to properly center the text. The text is also assigned a reflection effect for aesthetics.

Next, start() instantiates RotateTransition and configures the object. As well as invoking methods previously discussed in the context of other transition classes, RotateTransition's void setFromAngle(double value) and void setToAngle(double value) methods are invoked to set the starting and ending angle values to 0 and 360 degrees, respectively.

At this point, a scenegraph consisting of the text node is created by assigning it to a group and then assigning the group to the scene. The scene's width, height, and fill color (orange) are also specified; and a mouse-clicked listener is assigned to the scene so that clicking anywhere on its surface causes the rotation to be played or paused. Finally, the stage is configured. (I prevented the stage from being resized for aesthetics.)

Compile Listing 5 (javac RotateTDemo.java) and run the application (java RotateTDemo). You should observe the scene in Figure 5, which shows the reflected text rotating over the scene's background.

Figure 5

Figure 5 A reflected line of text rotates around the scene's center point.

Scale Transition

ScaleTransition changes a node property's scaleX, scaleY, and scaleZ properties from starting scaling values to ending scaling values over the duration specified by its duration property. The starting and ending values are specified via the following ScaleTransition properties and the current values of node's scaleX, scaleY, and scaleZ properties:

  • The starting values are specified by fromX, fromY, and fromZ, which wrap doubles. When not specified, the node's scaleX, scaleY, and scaleZ values are used instead.
  • The ending values are specified by toX, toY, and toZ, which wrap doubles. When not specified, the starting values plus the values of byX, byY, and byZ, which wrap doubles, are used instead. When the "to" and "by" properties are specified, the "to" properties take precedence.

ScaleTransition is especially useful in rich Internet applications that involve images. For example, as the mouse moves over a thumbnail image, the image scales up to a larger size, revealing more detail. I've created a Thumbnail component that demonstrates ScaleTransition in this scenario. Listing 6 presents the source code.

Listing 6—Thumbnail.java.

import javafx.animation.Animation;
import javafx.animation.ScaleTransition;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

import javafx.scene.Parent;

import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

import javafx.scene.paint.Color;

import javafx.scene.shape.Rectangle;

import javafx.util.Duration;


public class Thumbnail extends Parent
{
   public Thumbnail(String image)
   {
      final ScaleTransition stBig = new ScaleTransition();
      stBig.setNode(this);
      stBig.setFromX(1.0);
      stBig.setFromY(1.0);
      stBig.setToX(2.0);
      stBig.setToY(2.0);
      stBig.setDuration(new Duration(1000));

      final ScaleTransition stSmall = new ScaleTransition();
      stSmall.setNode(this);
      stSmall.setFromX(2.0);
      stSmall.setFromY(2.0);
      stSmall.setToX(1.0);
      stSmall.setToY(1.0);
      stSmall.setDuration(new Duration(1000));

      Rectangle rect = new Rectangle();
      rect.widthProperty().bind(widthProperty());
      rect.heightProperty().bind(heightProperty());
      rect.setArcWidth(5);
      rect.setArcHeight(5);
      rect.setStrokeWidth(4);
      rect.setStroke(Color.GRAY);

      ImageView iv = new ImageView(new Image(image));
      iv.setX(4);
      iv.setY(4);
      iv.fitWidthProperty().bind(widthProperty().subtract(8));
      iv.fitHeightProperty().bind(heightProperty().subtract(8));

      setOnMouseEntered(me ->
                        {
                           if (stSmall.getStatus() == Animation.Status.RUNNING)
                           {
                              stSmall.stop();
                              stBig.setFromX(stSmall.getNode().getScaleX());
                              stBig.setFromY(stSmall.getNode().getScaleY());
                           }
                           else
                           {
                              stBig.setFromX(1.0);
                              stBig.setFromY(1.0);
                           }

                           stBig.setToX(2.0);
                           stBig.setToY(2.0);

                           stBig.getNode().toFront();
                           stBig.playFromStart();
                        });

      setOnMouseExited(me ->
                       {
                          if (stBig.getStatus() == Animation.Status.RUNNING)
                          {
                             stBig.stop();
                             stSmall.setFromX(stBig.getNode().getScaleX());
                             stSmall.setFromY(stBig.getNode().getScaleY());
                          }
                          else
                          {
                             stSmall.setFromX(2.0);
                             stSmall.setFromY(2.0);
                          }

                          stSmall.setToX(1.0);
                          stSmall.setToY(1.0);

                          stSmall.playFromStart();
                       });

      getChildren().addAll(rect, iv);
   }

   private final DoubleProperty width = new SimpleDoubleProperty(0.0);

   public final void setWidth(double value) 
   {
      width.set(value);
   }

   public final double getWidth() 
   {
      return width.get();
   }

   public final DoubleProperty widthProperty() 
   {
      return width;
   }

   private final DoubleProperty height = new SimpleDoubleProperty(0.0);

   public final void setHeight(double value) 
   {
      height.set(value);
   }

   public final double getHeight() 
   {
      return height.get();
   }

   public final DoubleProperty heightProperty() 
   {
      return height;
   }
}

Thumbnail extends javafx.scene.Parent, which makes it possible to manage child nodes: a Thumbnail node consists of an ImageView node over a javafx.scene.shape.Rectangle node. This class supplies a constructor and the necessary methods for defining and managing width and height properties, which wrap doubles.

The constructor first creates two ScaleTransition objects to scale an image up and down. Each object's fromX and fromY properties are initialized via void setFromX(double value) and void setFromY(double value) method calls. Scale up doubles the image size, whereas scale down halves the size. Each transition lasts for one second.

The constructor next creates the Rectangle node. The rectangle's width and height properties are bound to the thumbnail's width and height properties so that the rectangle will resize whenever the thumbnail resizes—the rectangle serves as the thumbnail's background.

Continuing, the constructor creates the ImageView node that overlays the Rectangle node. The image argument that's passed to the constructor is passed to Image's constructor, which takes care of loading the image. The resulting Image object is passed to ImageView's constructor.

The image view's position is offset by four pixels and its extents are offset by eight pixels so that the rectangle's outline shows through (giving a nice aesthetic). The image view's fitWidth and fitHeight properties are bound to the thumbnail's width and height properties (less eight pixels) so that image view extents synchronize with thumbnail extents.

The constructor now registers mouse-entered and mouse-exited listeners. When the mouse enters the thumbnail node, the listener configures the scale-up transition's fromX/fromY and toX/toY properties. Then, it brings to the front the node being scaled up so that it doesn't partly appear behind an adjacent node, and then starts the transition.

When the mouse exits the thumbnail node, the listener configures the scale-down transition's fromX/fromY and toX/toY properties to handle reverse scaling. It doesn't have to bring the node to the front because the node that's scaling down is already in front and remains there until another thumbnail is entered. Finally, the listener starts this transition.

The constructor's final task is to add the rectangle and image view nodes to the thumbnail as its children.

We need an application that demonstrates Thumbnail and the scale-up and scale-down transitions. Listing 7 presents the source code to a ScaleTDemo application that creates a row of thumbnail nodes and lets you scale up and scale down any node via the mouse.

Listing 7—ScaleTDemo.java.

import javafx.application.Application;

import javafx.scene.Group;
import javafx.scene.Scene;

import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

import javafx.stage.Stage;

import javafx.util.Duration;

public class ScaleTDemo extends Application
{
   final static int NUMIMAGES = 5;

   @Override
   public void start(Stage primaryStage)
   {
      Stop[] stops = new Stop[] 
      { 
         new Stop(0.0, Color.YELLOW), 
         new Stop(0.5, Color.ORANGE),
         new Stop(1.0, Color.PINK)
      };
      LinearGradient lg = new LinearGradient(0, 0, 1, 1, true,
                                             CycleMethod.NO_CYCLE,
                                             stops);
      Group root = new Group();
      Scene scene = new Scene(root, 750, 400, lg);

      Thumbnail[] thumbnails = new Thumbnail[NUMIMAGES];
      for (int i = 0; i < NUMIMAGES; i++)
      {
         thumbnails[i] = new Thumbnail("res/photo" + (i + 1) + ".jpg");
         thumbnails[i].setWidth(100);
         thumbnails[i].setHeight(100);
         thumbnails[i].setTranslateX((scene.getWidth() + 10 -
                                     NUMIMAGES * thumbnails[i].getWidth() -
                                     10 * (NUMIMAGES - 1)) / 2 +
                                     i * (thumbnails[i].getWidth() + 10));
         thumbnails[i].setTranslateY((scene.getHeight() - 
                                     thumbnails[i].getHeight()) / 2);
         root.getChildren().add(thumbnails[i]);
      }

      scene.widthProperty()
      .addListener((observable, oldValue, newValue) ->
                   {
                      for (int i = 0; i < NUMIMAGES; i++)
                      {
                         double w = thumbnails[i].getLayoutBounds().getWidth();
                         thumbnails[i].setTranslateX((newValue.doubleValue() + 
                                                     10 - NUMIMAGES * w -
                                                     10 * (NUMIMAGES - 1)) / 2 +
                                                     i * (w + 10));
                      }
                   });
      scene.heightProperty()
      .addListener((observable, oldValue, newValue) ->
                   {
                      for (int i = 0; i < NUMIMAGES; i++)
                      {
                         double h = thumbnails[i].getLayoutBounds().getHeight();
                         thumbnails[i].setTranslateY((newValue.doubleValue() - 
                                                     h) / 2);
                      }
                   });

      primaryStage.setTitle("ScaleTransition Demo");
      primaryStage.setScene(scene);
      primaryStage.show();
   }
}

start() first creates a colorful linear gradient for the scene's background. It then creates a group container for the scenegraph and creates the scene. At this point, a Thumbnail array is created and populated with Thumbnail objects. After setting each object's width and height, the object is translated to its appropriate scene position and then added to the group.

Next, start() adds change listeners to the scene's width and height properties to update each Thumbnail object's translateX or translateY property when the scene's width or height changes, so that the row of thumbnails remains centered on the stage. (I cannot accomplish this task through binding.) The stage is then configured and shown.

Compile Listings 6 and 7 (javac ScaleTDemo.java) and run the application (java ScaleTDemo). You should observe the scene in Figure 6, which features a centered row of thumbnails with the center thumbnail scaled up.

Figure 6

Figure 6 The center thumbnail is scaled up and brought to the front.

Stroke Transition

StrokeTransition changes a shape property's stroke property from a starting color value to an ending color value over the duration specified by its duration property. The starting and ending color values are specified via the following StrokeTransition properties and the current value of shape's stroke property:

  • fromValue provides the transition's starting color value and wraps a Color. When not specified, the shape's stroke color value is used instead.
  • toValue provides the transition's ending color value and wraps a Color.

I've created a StrokeTDemo application that demonstrates StrokeTransition. This application uses this class to animate a circle's stroke color repeatedly from yellow to orange and then back to yellow in four seconds. Listing 8 presents the source code, which is very similar to FillTDemo's source code.

Listing 8—StrokeTDemo.java.

import javafx.animation.StrokeTransition;
import javafx.animation.Timeline;

import javafx.application.Application;

import javafx.scene.Group;
import javafx.scene.Scene;

import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

import javafx.scene.shape.Circle;

import javafx.stage.Stage;

import javafx.util.Duration;

public class StrokeTDemo extends Application
{
   final static int SCENE_WIDTH = 300;
   final static int SCENE_HEIGHT = 300;

   @Override
   public void start(Stage primaryStage)
   {
      Stop[] stops = new Stop[] 
      { 
         new Stop(0, Color.BLUE), 
         new Stop(1, Color.DARKBLUE)
      };
      LinearGradient lg = new LinearGradient(0, 0, 0, 1, true,
                                             CycleMethod.NO_CYCLE,
                                             stops);

      Group root = new Group();
      Scene scene = new Scene(root, SCENE_WIDTH, SCENE_HEIGHT, lg);

      Circle circle = new Circle();
      circle.centerXProperty().bind(scene.widthProperty().divide(2));
      circle.centerYProperty().bind(scene.heightProperty().divide(2));
      circle.setRadius(100);
      circle.setFill(Color.YELLOW);
      circle.setStroke(Color.YELLOW);
      circle.setStrokeWidth(4);

      root.getChildren().add(circle);

      StrokeTransition st = new StrokeTransition();
      st.setShape(circle);
      st.setDuration(new Duration(2000));
      st.setToValue(Color.ORANGE);
      st.setCycleCount(Timeline.INDEFINITE);
      st.setAutoReverse(true);
      st.play();

      primaryStage.setTitle("StrokeTransition Demo");
      primaryStage.setScene(scene);
      primaryStage.show();
   }
}

Compile Listing 8 (javac StrokeTDemo.java) and run the application (java StrokeTDemo). You should observe the scene in Figure 7, which consists of a yellow circle whose yellow border transitions to orange and back to yellow.

Figure 7

Figure 7 The circle's stroke color repeatedly transitions between yellow and orange.

Translate Transition

TranslateTransition changes a node property's translateX, translateY, and translateZ properties from starting translation values to ending translation values over the duration specified by its duration property. The starting and ending values are specified via the following TranslateTransition properties and the current values of node's translateX, translateY, and translateZ properties:

  • The starting values are specified by fromX, fromY, and fromZ, which wrap doubles. When not specified, the node's translateX, translateY, and translateZ values are used instead.
  • The ending values are specified by toX, toY, and toZ, which wrap doubles. When not specified, the starting values plus the values of byX, byY, and byZ, which wrap doubles, are used instead. When the "to" and "by" properties are specified, the "to" properties take precedence.

TranslateTransition is especially useful in a text-scrolling context. I've created a TranslateTDemo application that uses TranslateTransition to scroll text repeatedly from a horizontal starting position to the right of the right side of a rectangle, to an ending position to the left of the rectangle's left side, and then back to the right of its right side. Because of clipping, only that portion of the text appearing within the rectangle is visible. Listing 9 presents the source code.

Listing 9—TranslateTDemo.java.

import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.animation.TranslateTransition;

import javafx.application.Application;

import javafx.geometry.VPos;

import javafx.scene.Group;
import javafx.scene.Scene;

import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;

import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

import javafx.scene.shape.Rectangle;

import javafx.scene.text.Font;
import javafx.scene.text.Text;

import javafx.stage.Stage;

import javafx.util.Duration;

public class TranslateTDemo extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      LinearGradient lg = new LinearGradient(0.0, 0.0, 0.0, 1.0, true,
                                             CycleMethod.NO_CYCLE,
                                             new Stop(0.0, Color.BLUE),
                                             new Stop(1.0, Color.LIGHTSKYBLUE));
      Group root = new Group();
      Scene scene = new Scene(root, lg);

      Rectangle r = new Rectangle();
      r.setWidth(200);
      r.setHeight(80);
      r.setArcWidth(10);
      r.setArcHeight(10);
      r.translateXProperty().bind(scene.widthProperty().
                                  subtract(r.getLayoutBounds().getWidth()).
                                  divide(2));
      r.translateYProperty().bind(scene.heightProperty().
                                  subtract(r.getLayoutBounds().getHeight()).
                                  divide(2));
      r.setFill(Color.LIGHTYELLOW);

      root.getChildren().add(r);

      Text text = new Text("TranslateTransition Demo");
      text.setFont(new Font("Times New Roman BOLD", 22));
      text.setTextOrigin(VPos.TOP);
      text.setFill(Color.MEDIUMBLUE);
      text.setEffect(new DropShadow(BlurType.GAUSSIAN, Color.GRAY, 1, 0.25, 2, 
                                    2));
      root.getChildren().add(text);

      TranslateTransition tt = new TranslateTransition();
      tt.setNode(text);

      tt.fromXProperty().bind(r.translateXProperty().
                                add(r.getLayoutBounds().getWidth() + 20));
      tt.fromYProperty().bind(r.translateYProperty().
                                add((r.getLayoutBounds().getHeight() - 
                                    text.getLayoutBounds().getHeight()) / 2));

      tt.toXProperty().bind(r.translateXProperty().
                              subtract(text.getLayoutBounds().getWidth() + 20));
      tt.toYProperty().bind(tt.fromYProperty());
      tt.setDuration(new Duration(5000));
      tt.setInterpolator(Interpolator.LINEAR);
      tt.setAutoReverse(true);
      tt.setCycleCount(Timeline.INDEFINITE);

      scene.widthProperty().addListener((observable, oldValue, newValue) -> 
                                        {
                                           tt.playFromStart();
                                        });

      scene.heightProperty().addListener((observable, oldValue, newValue) -> 
                                         {
                                           tt.playFromStart();
                                         });

      Rectangle rClip = new Rectangle();
      rClip.setWidth(200);
      rClip.setHeight(80);
      rClip.setArcWidth(10);
      rClip.setArcHeight(10);
      rClip.translateXProperty().bind(r.translateXProperty());
      rClip.translateYProperty().bind(r.translateYProperty());
      root.setClip(rClip);

      primaryStage.setTitle("TranslateTransition Demo");
      primaryStage.setScene(scene);
      primaryStage.setWidth(400);
      primaryStage.setHeight(200);
      primaryStage.show();
   }
}

Listing 9 should be fairly easy to understand because of my descriptions of the previous applications. However, you might wonder why I've installed change listeners on the scene's width and height properties to restart the translation transition whenever the scene's width or height changes. You might think that this step shouldn't be necessary because of the binding operations.

The change listeners are necessary for the following reason: While discussing the fade transition, I mentioned that transition properties cannot be changed while a transition is running; such changes are ignored. You must stop and restart an animation to have the new property value(s) picked up by the transition. Comment out the change listeners and resize the scene. Although the rectangle remains centered, the text doesn't, and it slowly disappears because it is clipped.

Compile Listing 9 (javac TranslateTDemo.java) and run the application (java TranslateTDemo). You should observe the Figure 8 scene, in which the message TranslateTransition Demo scrolls over a rectangle.

Figure 8

Figure 8 The scrolling restarts whenever you resize the scene.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020