Using Transitions to Simplify JavaFX Animations
- Basic Transitions
- Compound Transitions
- Custom Transitions
- Conclusion
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 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 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 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 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 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 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 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 The scrolling restarts whenever you resize the scene.