Basic Transitions
The javafx.animation.transition package provides classes for performing six common basic transitions. You can use these classes to animate a node's opacity variable, move a node along a path, do nothing for awhile and then perform an action, animate a node's rotate variable, animate the node's scaleX and scaleY variables, or animate its translateX and translateY variables.
Fade
The FadeTransition class fades a node by transitioning its node variable's opacity member from a starting value to an ending value over the duration specified by its duration variable. The starting and ending values are specified via FadeTransition variables and node's current opacity value:
- The starting value is specified by fromValue (of type Number) or (if not present) the node's current opacity value.
- The ending value is specified by toValue (of type Number) or (if not present) the starting value plus the value of variable byValue (of type Number). If both toValue and byValue are specified, toValue takes precedence.
The fade transition is commonly employed by slideshow applications for transitioning between successive slides. For example, I demonstrate FadeTransition in the slideshow application in my article "Deploying a JavaFX Application."
For this article, I've chosen something simpleran application that animates a single image's opacity from opaque to transparent and back again three times over a 12-second duration when you click the image. Listing 1 presents the application's Main.fx source file (excerpted from a NetBeans IDE 6.5.1 FadeTDemo project).
Listing 1Main.fx (from a FadeTDemo project).
/* * Main.fx */ package fadetdemo; import javafx.animation.transition.FadeTransition; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; def image = Image { url: "{__DIR__}res/flowers.jpg" } Stage { title: "FadeTransition Demo" scene: Scene { width: image.width height: image.height var iv: ImageView content: iv = ImageView { image: image def fadeT = FadeTransition { node: bind iv duration: 2s fromValue: 1.0 toValue: 0.0 repeatCount: 6 autoReverse: true } onMouseClicked: function (me: MouseEvent): Void { fadeT.play () } } } }
The application loads an image into a javafx.scene.image.Image object and makes the stage's scene just large enough to present the image in its entirety. A FadeTransition is created, and the javafx.scene.image.ImageView node that will present the image is bound to the transition's node variable. The other variables accomplish the following tasks:
- duration specifies 2s (2000 milliseconds) as the length of a forward transition (fromValue to toValue) or a reverse transition (toValue to fromValue) cycle.
- fromValue specifies 1.0 (opaque) as the starting opacity for a forward transition cycle and the ending opacity for a reverse transition cycle.
- toValue specifies 0.0 (transparent) as the ending opacity for a forward transition cycle and the starting opacity for a reverse transition cycle.
- repeatCount specifies 6 as the number of transition cyclesthree forward and three reverse.
- autoReverse specifies true to indicate that each transition cycle runs in reverse to the previous cycle.
The ImageView node's onMouseClicked handler starts the FadeTransition (unless it's already running) whenever the mouse is clicked over the displayed image. The click initiates three transitions: opaque-to-transparent (where the white background is fully visible) followed by transparent-to-opaque (where the image is completely visible). Figure 1 shows one instance of this sequence.
Figure 1 Fading out a garden of flowers. (Image courtesy of Lydia Jacobs at Public Domain Pictures.)
Path
The PathTransition class moves a node along a geometric path by transitioning its node variable's translateX and translateY members along its path variable's AnimationPath over the duration assigned to its duration variable. The node's rotate member is also regularly updated if OrientationType.ORTHOGONAL_TO_TANGENT is assigned to PathTransition's orientation variable.
PathTransition works with two supporting classes:
- AnimationPath provides three functions:
public createFromPath(path: Path): AnimationPath public createFromPath(svgPath: SVGPath): AnimationPath public createFromShape(shape: Shape): AnimationPath
javafx.scene.shape.Path javafx.scene.shape.SVGPath javafx.scene.shape.Shape
I've chosen to demonstrate these classes in a simulation of a car moving over a road. Listing 2 shows this simulation's source code.
Listing 2Main.fx (from a PathTDemo project).
/* * Main.fx */ package pathtdemo; import javafx.animation.Interpolator; import javafx.animation.Timeline; import javafx.animation.transition.AnimationPath; import javafx.animation.transition.OrientationType; import javafx.animation.transition.PathTransition; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; 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.stage.Stage; def car: ImageView = ImageView { var carim: Image image: carim = Image { url: "{__DIR__}res/car.gif" } x: bind -carim.width/2 y: bind 300-carim.height/2 rotate: 90 } def path = [ MoveTo { x: 0 y: 300 } ArcTo { x: 100 y: 400 radiusX: 100 radiusY: 100 } LineTo { x: 300 y: 400 } ArcTo { x: 400 y: 300 radiusX: 100 radiusY: 100 } LineTo { x: 400 y: 100 } ArcTo { x: 300 y: 0 radiusX: 100 radiusY: 100 } LineTo { x: 100 y: 0 } ArcTo { x: 0 y: 100 radiusX: 100 radiusY: 100 } LineTo { x: 0 y: 300 } ClosePath {} ]; def road = Path { stroke: Color.BLACK strokeWidth: 75 elements: path } def divider = Path { stroke: Color.WHITE strokeWidth: 4 strokeDashArray: [ 10, 10 ] elements: path } def anim = PathTransition { node: car path: AnimationPath.createFromPath (road) orientation: OrientationType.ORTHOGONAL_TO_TANGENT interpolate: Interpolator.LINEAR duration: 6s repeatCount: Timeline.INDEFINITE } Stage { title: "PathTransition Demo" width: 510 height: 535 scene: Scene { fill: Color.DARKGREEN content: Group { content: [ road, divider, car ] translateX: 50 translateY: 50 onMouseClicked: function (me: MouseEvent): Void { if (anim.running and not anim.paused) anim.pause () else anim.play () } } } }
Listing 2 first creates an ImageView node that's initialized to the image of a car being animated. This node is given an appropriate starting position on the road, which happens to be centered over the divider line. It's also 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 linethey 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 ClosePath {} literal is required to close the path properly.
The listing next creates the road and divider line nodes, breaking the divider line into visible and transparent segments of equal length by initializing its strokeDashArray variable. The divider line will appear in the exact middle of the road because the road is essentially just a "fat" divider line, and they both share the same coordinates.
The PathTransition object connects the car and road nodes by assigning the car node to this object's node variable, and by indirectly assigning the road shape node to the variable path via AnimationPath's createFromPath() function. Rounding out the initialization are instructions to keep the car node perpendicular to its path, and to specify an indefinite number of six-second road tours.
The scene is specified via the 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's 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 contains 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. Figure 2 reveals the car turning a corner as it travels around the road.
Figure 2 The center of the node being animated (the car) serves as the animation's anchor pointit follows the path most closely.
As an aside, you can change the car's color via the javafx.scene.effect.ColorAdjust class. After instantiating this class, you'll typically only need to assign a color value to the instance's hue variable (of type Number). For example, the following code fragment, excerpted from Listing 2 (with additional code shown in boldface), changes the car's color to orange by assigning 0.15 to hue:
def car = ImageView { var carim: Image image: carim = Image { url: "{__DIR__}res/car.gif" } x: bind -carim.width/2 y: bind 300-carim.height/2 rotate: 90 effect: javafx.scene.effect.ColorAdjust { hue: 0.15 } }
Pause
The PauseTransition class waits for its duration value to expire and then executes its action variable's function. Although you can forget about the function if you're only interested in the pause, this function comes in handy when you're building a button component and simulating the javax.swing.AbstractButton class's public void doClick() method (see Listing 3).
Listing 3Main.fx (from a PauseTDemo project).
/* * Main.fx */ package pausetdemo; import javafx.animation.transition.PauseTransition; import javafx.scene.CustomNode; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; import javafx.scene.paint.LinearGradient; import javafx.scene.paint.Paint; import javafx.scene.paint.Stop; import javafx.scene.shape.Rectangle; import javafx.scene.text.Font; import javafx.scene.text.Text; import javafx.scene.text.TextOrigin; import javafx.stage.Stage; def BACKGROUND_PAINT = LinearGradient { startX: 0.0 startY: 0.0 endX: 0.0 endY: 1.0 stops: [ Stop { offset: 0.0 color: Color.BLACK }, Stop { offset: 1.0 color: Color.BLUEVIOLET } ] } Stage { title: "PauseTransition Demo" width: 300 height: 200 var scene: Scene scene: scene = Scene { fill: BACKGROUND_PAINT var button: Button; content: button = Button { x: bind (scene.width-100)/2 y: bind (scene.height-40)/2 width: 100 height: 40 arcWidth: 10 arcHeight: 10 text: "OK" font: Font { name: "Arial BOLD" size: 16 } fill: Color.DARKBLUE fillRollover: Color.MEDIUMBLUE fillPressed: Color.LIGHTBLUE borderColor: Color.WHITE textColor: Color.WHITE action: function (): Void { button.text = if (button.text == "OK") "OKAY" else "OK"; button.fillRollover = if (button.fill == Color.DARKBLUE) Color.MEDIUMVIOLETRED else Color.MEDIUMBLUE; button.fillPressed = if (button.fill == Color.DARKBLUE) Color.PALEVIOLETRED else Color.LIGHTBLUE; button.fill = if (button.fill == Color.DARKBLUE) Color.BLUEVIOLET else Color.DARKBLUE } } } } class Button extends CustomNode { public var text: String; public var font: Font; public var x: Number; public var y: Number; public var width: Number; public var height: Number; public var arcWidth: Number; public var arcHeight: Number; public var fill: Paint on replace oldFill { if (curFill == oldFill) curFill = fill } public var fillPressed: Paint on replace oldFillPressed { if (curFill == oldFillPressed) curFill = fillPressed } public var fillRollover: Paint on replace oldFillRollover { if (curFill == oldFillRollover) curFill = fillRollover } public var borderColor: Paint; public var textColor: Paint; public var action: function (): Void; var curFill: Paint; public override function create (): Node { Group { var r: Rectangle on replace { if (r != null) r.requestFocus () } var t: Text content: [ r = Rectangle { x: bind x y: bind y width: bind width height: bind height arcWidth: bind arcWidth arcHeight: bind arcHeight fill: bind curFill stroke: bind borderColor onMouseEntered: function (me: MouseEvent): Void { curFill = fillRollover } onMouseExited: function (me: MouseEvent): Void { curFill = fill } onMousePressed: function (me: MouseEvent): Void { curFill = fillPressed } onMouseReleased: function (me: MouseEvent): Void { curFill = if (r.contains (me.x, me.y)) fillRollover else fill } onMouseClicked: function (e: MouseEvent): Void { action () } onKeyPressed: function (e: KeyEvent): Void { if (e.code != KeyCode.VK_SPACE) return; def pause = PauseTransition { duration: 68ms action: function (): Void { curFill = fill; action () } } curFill = fillPressed; pause.play () } } t = Text { content: bind text font: bind font textOrigin: TextOrigin.TOP translateX: bind x+(r.layoutBounds.width- t.layoutBounds.width)/2 translateY: bind y+(r.layoutBounds.height- t.layoutBounds.height)/2 fill: bind textColor } ] } } }
Listing 3 creates and demonstrates a highly configurable button component. The key part of this listing is the code within Button's onKeyPressed handler function. In response to a key being pressed (the single button node's rectangle component requests keyboard focus), this handler function accomplishes the following tasks:
- Verifies that the spacebar key has been pressed. Pressing the spacebar is equivalent to clicking the mouse button over the button node.
- Creates a PauseTransition. For consistency with doClick(), this transition's duration member is set to 68ms.
- Sets the button's current-fill to the pressed-fill setting and plays the transition. When the transition completes, it resets the current fill to the default fill setting and invokes the button's action member.
Figure 3 reveals the button's state after the spacebar has been pressed once.
Figure 3 Both the button's text and fill colors change each time the spacebar is pressed or the mouse is clicked over the button.