Home > Articles > Programming > Java

  • Print
  • + Share This
From the author of

From the author of

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 simpler—an 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 1—Main.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 cycles—three 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
  • Each of these functions returns an AnimationPath for its applicable argument:
    • javafx.scene.shape.Path
      javafx.scene.shape.SVGPath
      javafx.scene.shape.Shape
  • OrientationType specifies the upright orientation of PathTransition's node along its path. Assigning constant NONE to PathTransition's orientation variable results in unchanged node rotation as the node moves along the path, whereas assigning constant ORTHOGONAL_TO_TANGENT to orientation results in the node continually being rotated so that it remains perpendicular to the path's tangent.

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 2—Main.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 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 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 point—it 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 3—Main.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.

  • + Share This
  • 🔖 Save To Your Account