2.6 Doing Things
JavaFX has three main constructs for doing things: binding, node properties that define event handlers, and animation. Together, these constructs provide powerful yet elegant solutions for modifying scene graphs based on user input or other events. Let’s see how GuitarTuner uses these constructs to get its tasks done.
Binding
Binding in JavaFX is a powerful technique and a concise alternative to specifying traditional callback event handlers. Basically, binding lets you make a property or variable depend on the value of an expression. When you update any of the “bound to” objects in the expression, the dependent object automatically changes. Suppose, for example, we bind area to height and width, as follows.
var height = 3.0; var width = 4.0; def area = bind height * width; // area = 12.0 width = 2.5; // area = 7.5 height = 4;// area = 10.0
When either height or width changes, so does area. Once you bind a property (or variable), you can’t update it directly. For example, you get a compile-time error if you try to directly update area.
area = 5;// compile time error
If you make area a var and provide a binding expression, you’ll get a runtime error if you try to update it directly.
In GuitarTuner, the vibrating string changes both its location (property translateY) and its thickness (property height) at run time to give the appearance of vibration. These properties are bound to other values that control how a guitar string node changes.
var vibrateH: Number; var vibrateY: Number; Rectangle { x: 0.0 y: yOffset width: stringLength height: bind vibrateH // change height when vibrateH changes fill: stringColor visible: false translateY: bind vibrateY // change translateY when vibrateY changes }
GuitarTuner also uses bind to keep the fret board centered vertically by binding property layoutY in the top level group.
group = Group { layoutY: bind (scene.height - group.layoutBounds.height) / 2 - group.layoutBounds.minY . . . }
Node property layoutBounds provides bounds information for its contents. If a user resizes the window, the top level group is automatically centered vertically on the screen. Binding helps reduce event processing code because (here, for example) you don’t have to write an event handler to detect a change in the window size.
Mouse Events
JavaFX nodes have properties for handling mouse and key events. These properties are set to callback functions that the system invokes when an event triggers. In GuitarTuner, the “mouse detection” rectangle has the following event handler to detect a mouse click event.
onMouseClicked: function(evt: MouseEvent): Void { if (evt.button == MouseButton.PRIMARY) { // play and vibrate selected “string” } }
The if statement checks for a click of the primary mouse button (generally the left mouse button is primary) before processing the event. The event handler function (shown in the next section) plays the note and vibrates the string.
Animations
JavaFX specializes in animations. (In fact, we dedicate an entire chapter to animation. See Chapter 7 beginning on page 205.) You define animations with timelines and then invoke Timeline functions play or playFromStart (there are also functions pause and stop). Timelines consist of a sequence of key frame objects that define a frame at a specific time offset within the timeline. (Key frames are declarative. You say “this is the state of the scene at this key time” and let the system figure out how to render the affected objects.) Within each key frame, you specify values, an action, or both. Traditionally, people think of animations as a way to move objects. While this is true, you’ll see that JavaFX lets you animate any writable object property. You could, for instance, use animation to fade, rotate, resize, or even brighten an image.
Figure 2.11 shows a snapshot of a program with simple animation. It moves a circle back and forth across its container.
Here is the timeline that implements this animation using a specialized shorthand notation for KeyFrames. The timeline starts out by setting variable x to 0. In gradual, linear increments, it changes x so that at four seconds, its value is 350. Now, it performs the action in reverse, gradually changing x so that in four more seconds it is back to 0 (autoReverse is true). This action is repeated indefinitely (or until the timeline is stopped or paused). Constants 0s and 4s are Duration literals.
var x: Number; Timeline { repeatCount: Timeline.INDEFINITE autoReverse: true keyFrames: [ at (0s) { x => 0.0 } at (4s) { x => 350 tween Interpolator.LINEAR } ] }.play(); // start Timeline . . . Circle { . . . translateX: bind x }
The JavaFX keyword tween is a key frame operator that lets you specify how a variable changes. Here, we use Interpolator.LINEAR for a linear change. That is, x doesn’t jump from 0 to 350, but gradually takes on values in a linear fashion. Linear interpolation moves the Circle smoothly from 0 to 350, taking four seconds to complete one iteration of the timeline.
JavaFX has other interpolators. Interpolator DISCRETE jumps from the value of one key frame to the second. Interpolator EASEIN is similar to LINEAR, except the rate of change is slower at the onset. Similarly, EASEOUT is slower at the finish and EASEBOTH provides easing on both ends of the timeline.
To make this animation apply to the Circle node, you bind the Circle’s translateX property to the variable manipulated by the timeline (x). Property translateX represents a node’s change in the x direction.
Now let’s examine how GuitarTuner uses animation to vibrate the guitar string and play its note. Each GuitarString object uses two rectangles to implement its visible behavior. One rectangle is a stationary, thin “string” and represents the string in a static state. This motionless rectangle is always visible in the scene. The second rectangle is only visible when the string is “played.” This rectangle expands and contracts its height quickly using animation (a Timeline). This moving rectangle gives users the illusion of a vibrating string.
To get a uniform vibrating effect, the rectangle must expand and contract evenly on the top and bottom. The animation makes the string appear to vibrate by varying the height of the rectangle from 1 to 3 while keeping it vertically centered by varying its translateY property between 5 and 4. When the string is clicked, the string’s note plays and the rectangle vibrates for the allotted time. When the timeline stops, only the stationary rectangle is visible.
Let’s first look at the timeline that plays the note. This timeline appears in the event handler for the GuitarString node (see the code for GuitarString in Listing 2.2 on page 38).
onMouseClicked: function(evt: MouseEvent): Void { if (evt.button == MouseButton.PRIMARY) { Timeline { keyFrames: [ KeyFrame { time: 0s action: playNote // play note and start vibration } KeyFrame { time: 2.8s action: stopNote // stop playing note and stop vibration } ] }.play(); // start Timeline } }
Here, the timeline is an object literal defined inside the event handler, invoked with function play. This timeline defines a sequence of KeyFrame objects, where function playNote is invoked at time offset 0 seconds and function stopNote is invoked at time offset 2.8 seconds (2.8s). Here are functions playNote and stopNote.
// play note and start vibration function playNote(): Void { synthNote.noteOn(note); vibrateOn(); } // stop playing note and stop vibration function stopNote(): Void { synthNote.noteOff(note); vibrateOff(); }
Function synthNote.noteOn calls a Java class API to play the guitar string. Function vibrateOn causes the string vibration.
function vibrateOn(): Void { play.visible = true; // make the vibrating rectangle visible timeline.play(); // start the vibration timeline }
Here is the vibration timeline.
def timeline = Timeline { repeatCount: Timeline.INDEFINITE autoReverse: true keyFrames: [ at (0s) { vibrateH => 1.0 } at (.01s) { vibrateH => 3.0 tween Interpolator.LINEAR } at (0s) { vibrateY => 5.0 } at (.01s) { vibrateY => 4.0 tween Interpolator.LINEAR } ] };
This timeline uses the shorthand notation discussed earlier for key frames and animates two variables: vibrateH and vibrateY. Variable vibrateH changes the height of the rectangle that represents the vibrating string. Variable vibrateY changes the vertical position of the rectangle to keep it centered as the oscillating height changes.