- What You Will Learn
- 3.1 What Is JavaFX?
- 3.2 Building JavaFX Programs
- 3.3 JavaFX Properties
- 3.4 Putting It All Together
- 3.5 Key Point Summary
3.4 Putting It All Together
Our final example in this chapter applies what you’ve learned about properties, binding, change listeners, and layout controls to create a program that simulates a race track with one car. As the car travels along the track, a lap counter updates each time the car passes the starting point. Figure 3.18 shows this program running at two points in time.
Figure 3.18 A Race Track with PathTransition and Lap Counter
This example pulls together several important concepts from this chapter: binding properties to keep values synchronized as the program runs; using a change listener to track changes in a property; and writing button event handlers that control the execution of the program. We’ll also show you how to organize nodes in a Group to keep items in their relative coordinate positions while still maintaining the layout’s overall positioning. We’ve shown you a RotateTransition example; now we’ll show you how to use PathTransition for the race track.
The program includes a Start/Pause button to start and pause the animation. Once you start, the speed up and slow down buttons alter the car’s travel rate. When the animation is paused (as shown in the left side figure), the Start/Pause button displays Start and the slower/faster buttons are disabled. When the animation is running (the right side), the Start/Pause button displays Pause and the slower/faster buttons are enabled.
We’ll implement the animation with PathTransition, a high-level Transition that animates a node along a JavaFX Path. Path is a Shape consisting of Path elements, where each element can be any one of several geometric objects, such as LineTo, ArcTo, QuadraticCurveTo, and CubicCurveTo. In our example, we build an oval track by combining Path elements MoveTo (the starting point), ArcTo, LineTo, and ClosePath (a specialized Path element that provides a LineTo from the current Path element point to the starting point).
Our race car is a rounded Rectangle and a Text node displays the current lap count. We implement this example using FXML. An associated controller class defines the buttons’ action event handlers, binding, and the PathTransition.
Figure 3.19 shows the scene graph structure. The top-level node is a VBox, which keeps its children (a StackPane and an HBox) in vertical alignment and centered. The StackPane also centers its child Group and Text nodes. The Group, in turn, consists of the Path, the track’s starting Line, and the race car (a Rectangle). These three nodes use the Group’s local coordinate system for their relative placement.
Figure 3.19 Scene graph hierarchy for project RaceTrackFXApp
The HBox maintains its children in a horizontal alignment. If you resize the JavaFX application window frame, these components all remain centered.
Listing 3.17 shows the FXML markup for the VBox, StackPane, Group, Text, and HBox nodes (we’ll show you the other nodes next). The scene graph hierarchy from Figure 3.19 matches the FXML elements shown in RaceTrack.fxml. The top node, VBox, specifies the controller class with attribute fx:controller. Note that we also supply an fx:id="text" attribute with the Text node. This lets the Controller class access the Text object in Java controller code.
Listing 3.17 RaceTrack.fxml
<?xml version="1.0" encoding="UTF-8"?> <?import java.lang.*?> <?import java.util.*?> <?import javafx.scene.*?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <?import javafx.scene.shape.*?> <?import javafx.scene.text.*?> <?import javafx.scene.effect.*?> <VBox id="VBox" prefHeight="300" prefWidth="400" spacing="20" alignment="CENTER" style="-fx-background-color: lightblue;" xmlns:fx="http://javafx.com/fxml" fx:controller="racetrackfxapp.RaceTrackController"> <children> <StackPane > <children> <Group> <children> (See Listing 3.18) </children> </Group> <Text fx:id="text" > <font><Font name="Verdana" size="16" /></font> <effect><Reflection /></effect> </Text> </children> </StackPane> <HBox spacing="20" alignment="CENTER" > (See Listing 3.19) </HBox> </children> </VBox>
Listing 3.18 shows the FXML for the Group’s children: the Path, Line, and Rectangle nodes. The Path node includes the elements that form the oval RaceTrack: MoveTo, ArcTo, LineTo, and ClosePath. The Line node marks the starting line on the track. The Rectangle node represents the “race car.” Nodes Path and Rectangle also have fx:id attributes defined for Controller access. Both the Path and Line nodes define a DropShadow effect.
Listing 3.18 Path, Line, and Rectangle Nodes
<Group> <children> <Path fx:id="path" stroke="DARKGOLDENROD" strokeWidth="15" fill="orange" > <effect> <DropShadow fx:id="dropshadow" radius="10" offsetX="5" offsetY="5" color="GRAY" /> </effect> <elements> <MoveTo x="0" y="0" /> <ArcTo radiusX="100" radiusY="50" sweepFlag="true" x="270" y="0" /> <LineTo x="270" y="50" /> <ArcTo radiusX="100" radiusY="50" sweepFlag="true" x="0" y="50" /> <ClosePath /> </elements> </Path> <Line startX="-25" startY="0" endX="10" endY="0" strokeWidth="4" stroke="BLUE" strokeLineCap="ROUND" effect="$dropshadow" /> <Rectangle fx:id="rectangle" x="-15" y="0" width="35" height="20" fill="YELLOW" arcWidth="10" arcHeight="10" stroke="BLACK" rotate="90" /> </children> </Group>
Listing 3.19 shows the FXML for the three Button nodes that appear in the HBox layout pane. All three buttons include fx:id attributes because they participate in binding expressions within the Controller class. The onAction attribute specifies the action event handler defined for each button. These event handlers control the PathTransition’s animation. The startPauseButton configures property prefWidth. This makes the button maintain a constant size as the button’s text changes between “Start” and “Pause.”
Listing 3.19 HBox and Button Nodes
<HBox spacing="20" alignment="CENTER" > <Button fx:id="slowerButton" onAction="#slowerAction" /> <Button fx:id="startPauseButton" prefWidth="80" onAction="#startPauseAction" /> <Button fx:id="fasterButton" onAction="#fasterAction" /> </HBox>
Now that the UI is completely described with FXML, let’s examine the Controller class, class RaceTrackController, as shown in Listing 3.20 through Listing 3.23. The @FXML annotations mark each variable created in the FXML that the Controller class needs to access. Recall that the FXML Loader is responsible for instantiating these objects, so you won’t see any Java code that creates them.
The initialize() method is invoked by the FXML Loader after the scene graph objects are instantiated. Here we perform additional scene graph configuration. Specifically, we instantiate the PathTransition (the animation object responsible for moving the “car” along the RaceTrack path). This high-level animation applies to a node (here, a Rectangle). The orientation property (OrientationType.ORTHOGONAL_TO_TANGENT) keeps the rectangle correctly oriented as it moves along the path. We set the duration, cycle count, and interpolator, making the cycle count INDEFINITE. The animation rate remains constant with Interpolator.LINEAR.
Listing 3.20 RaceTrackController.java
package racetrackfxapp; import java.net.URL; import java.util.ResourceBundle; import javafx.animation.Animation; import javafx.animation.Interpolator; import javafx.animation.PathTransition; import javafx.beans.binding.When; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Button; import javafx.scene.shape.Path; import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; import javafx.util.Duration; public class RaceTrackController implements Initializable { // Objects defined in the FXML @FXML private Rectangle rectangle; @FXML private Path path; @FXML private Text text; @FXML private Button startPauseButton; @FXML private Button slowerButton; @FXML private Button fasterButton; private PathTransition pathTransition; @Override public void initialize(URL url, ResourceBundle rb) { // Create the PathTransition pathTransition = new PathTransition(Duration.seconds(6), path, rectangle); pathTransition.setOrientation( PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT); pathTransition.setCycleCount(Animation.INDEFINITE); pathTransition.setInterpolator(Interpolator.LINEAR); . . . additional code from method initialize() shown in Listing 3.21, Listing 3.22, and Listing 3.23 . . . } }
Listing 3.21 shows how the Lap Counter works. Here, we create a JavaFX property called lapCounterProperty by instantiating SimpleIntegerProperty, an implementation of abstract class IntegerProperty. We need a JavaFX property here because we use it in a binding expression. (We discuss creating your own JavaFX properties in detail in the next chapter; see “Creating JavaFX Properties” on page 132.)
Next, we create a ChangeListener (with a lambda expression) and attach it to the PathTransition’s currentTime property. We count laps by noticing when the animation’s currentTime property old value is greater than its new value. This happens when one lap completes and the next lap begins. We then increment lapCounterProperty.
We create a binding expression to display the Lap Counter in the Text component. We bind the Text’s text property to the lapCounterProperty, using the Fluent API to convert the integer to a String and provide the formatting.
Note that the ChangeListener method is invoked frequently—each time the current time changes. However, inside the listener method, we only update lapCounterProperty once per lap, ensuring that the Text node’s text property only updates once a lap. This two-step arrangement makes the related binding with the Text node an efficient way to keep the UI synchronized.
Listing 3.21 Configuring the Lap Counter Binding
// We count laps by noticing when the currentTimeProperty changes and the // oldValue is greater than the newValue, which is only true once per lap // We then increment the lapCounterProperty final IntegerProperty lapCounterProperty = new SimpleIntegerProperty(0); pathTransition.currentTimeProperty().addListener( (ObservableValue<? extends Duration> ov, Duration oldValue, Duration newValue) -> { if (oldValue.greaterThan(newValue)) { lapCounterProperty.set(lapCounterProperty.get() + 1); } }); // Bind the text's textProperty to the lapCounterProperty and format it text.textProperty().bind(lapCounterProperty.asString("Lap Counter: %s"));
The Start/Pause button lets you start and pause the animation, as shown in Listing 3.22. The @FXML annotations before the action event handlers make the FXML Loader wire the Start/Pause button’s onAction property with the proper event handler. This invokes the handler when the user clicks the button. If the animation is running, method pause() pauses it. Otherwise, the animation is currently paused and the play() method either starts or resumes the animation at its current point.
The button’s text is controlled with binding object When, the beginning point of a ternary binding expression. Class When takes a boolean condition that returns the value in method then() if the condition is true or the value in method otherwise() if it’s false. For the Start/Pause button, we set its text to “Pause” if the animation is running; otherwise, we set the text to “Start.”
Listing 3.22 Start/Pause Button
@FXML private void startPauseAction(ActionEvent event) { if (pathTransition.getStatus() == Animation.Status.RUNNING) { pathTransition.pause(); } else { pathTransition.play(); } } . . . startPauseButton.textProperty().bind( new When(pathTransition.statusProperty() .isEqualTo(Animation.Status.RUNNING)) .then("Pause").otherwise("Start"));
Listing 3.23 shows the faster/slower button handler code. Both buttons’ action event handlers (annotated with @FXML to wire them to the respective Buttons defined in the FXML file) manipulate the animation’s rate property. We specify a maximum rate of 7.0 (seven times the default rate) and a minimum rate of .3. Each time the user clicks the faster or slower button the rate changes up or down by .3. We set the new rate by accessing the transition’s currentRate property. The printf() statements let you see the changed rates for each button click.
The faster/slower buttons are disabled when the animation is not running. This is accomplished with binding expressions that check the animation’s status. Finally, the controller sets the text of the faster/slower buttons.
Listing 3.23 Faster and Slower Buttons
// Constants to control the transition's rate changes final double maxRate = 7.0; final double minRate = .3; final double rateDelta = .3; @FXML private void slowerAction(ActionEvent event) { double currentRate = pathTransition.getRate(); if (currentRate <= minRate) { return; } pathTransition.setRate(currentRate - rateDelta); System.out.printf("slower rate = %.2f\n", pathTransition.getRate()); } @FXML private void fasterAction(ActionEvent event) { double currentRate = pathTransition.getRate(); if (currentRate >= maxRate) { return; } pathTransition.setRate(currentRate + rateDelta); System.out.printf("faster rate = %.2f\n", pathTransition.getRate()); } . . . fasterButton.disableProperty().bind(pathTransition.statusProperty() .isNotEqualTo(Animation.Status.RUNNING)); slowerButton.disableProperty().bind(pathTransition.statusProperty() .isNotEqualTo(Animation.Status.RUNNING)); fasterButton.setText(" >> "); slowerButton.setText(" << ");
A key point with this example is that JavaFX properties and bindings let you write much less code. To enable and disable the buttons without binding, you would need to create a change listener, attach it to the animation’s status property, and write an event handler that updates the button’s disable property. You would also need a similar change listener to control the Start/Pause button’s text property. Binding expressions (including custom binding objects) are much more concise and less error prone than listeners that configure property dependencies such as these.
However, at times you will need a ChangeListener or InvalidationListener, as we show in this example to implement the lap counter.
This example also shows how FXML markup helps you visualize the structure of the scene graph that you’re building. Working with FXML, as opposed to only Java APIs, makes it easier to modify scene graphs. We’ve also built this example using only APIs (see project RaceTrack in the book’s download). We encourage you to compare the two projects. We think you’ll find the FXML version easier to understand.