Graphics with JavaFX - Java 8 Recipes, 2th Edition (2014)

Java 8 Recipes, 2th Edition (2014)

CHAPTER 15. Graphics with JavaFX

Have you ever heard someone say, “When two worlds collide”? This expression is used when a person from a different background or culture is put in a situation where they are at odds and must face very hard decisions. When we build a GUI application requiring animations, we are often in a collision course between business and gaming worlds.

In the ever-changing world of rich client applications, you probably have noticed an increase of animations such as pulsing buttons, transitions, moving backgrounds, and so on. When GUI applications use animations, they can provide visual cues to the users to let them know what to do next. With JavaFX, you can have the best of both worlds.

Figure 15-1 illustrates a simple drawing coming alive.

9781430268277_Fig15-01.jpg

Figure 15-1. Graphics with JavaFX

In this chapter you will create images, animations, and Look and Feels. Fasten your seatbelts; you’ll discover solutions to integrate cool game-like interfaces into your everyday applications.

Image Note Refer to Chapter 14 if you are new to JavaFX. Among other things, it will help you create an environment in which you can be productive using JavaFX.

15-1. Creating Images

Problem

There are photos in your file directory that you would like to quickly browse through and showcase.

Solution

Create a simple JavaFX image viewer application. The main Java classes used in this recipe are:

· javafx.scene.image.Image

· javafx.scene.image.ImageView

· EventHandler<DragEvent> classes

The following source code is an implementation of an image viewer application:

package org.java8recipes.chapter15.recipe15_01;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
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.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

/**
* Recipe 15-1: Creating Images
*
* @author cdea
* Update: J Juneau
*/
public class CreatingImages extends Application {

private final List<String> imageFiles = new ArrayList<>();
private int currentIndex = -1;
private final String filePrefix = "file:";

public enum ButtonMove {

NEXT, PREV
};

/**
* @param args the command line arguments
*/
public static void main(String[] args) {
Application.launch(args);
}

@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Chapter 15-1 Creating a Image");
Group root = new Group();
Scene scene = new Scene(root, 551, 400, Color.BLACK);

// image view
final ImageView currentImageView = new ImageView();

// maintain aspect ratio
currentImageView.setPreserveRatio(true);

// resize based on the scene
currentImageView.fitWidthProperty().bind(scene.widthProperty());

final HBox pictureRegion = new HBox();
pictureRegion.getChildren().add(currentImageView);
root.getChildren().add(pictureRegion);

// Dragging over surface
scene.setOnDragOver((DragEvent event) -> {
Dragboard db = event.getDragboard();
if (db.hasFiles()) {
event.acceptTransferModes(TransferMode.COPY);
} else {
event.consume();
}
});

// Dropping over surface
scene.setOnDragDropped((DragEvent event) -> {
Dragboard db = event.getDragboard();
boolean success = false;
if (db.hasFiles()) {
success = true;
String filePath = null;
for (File file : db.getFiles()) {
filePath = file.getAbsolutePath();
System.out.println(filePath);
currentIndex += 1;
imageFiles.add(currentIndex, filePath);
}
filePath = filePrefix + filePath;
// set new image as the image to show.
Image imageimage = new Image(filePath);
currentImageView.setImage(imageimage);

}
event.setDropCompleted(success);
event.consume();

});

// create slide controls
Group buttonGroup = new Group();

// rounded rect
Rectangle buttonArea = new Rectangle();
buttonArea.setArcWidth(15);
buttonArea.setArcHeight(20);
buttonArea.setFill(new Color(0, 0, 0, .55));
buttonArea.setX(0);
buttonArea.setY(0);
buttonArea.setWidth(60);
buttonArea.setHeight(30);
buttonArea.setStroke(Color.rgb(255, 255, 255, .70));

buttonGroup.getChildren().add(buttonArea);
// left control
Arc leftButton = new Arc();
leftButton.setType(ArcType.ROUND);
leftButton.setCenterX(12);
leftButton.setCenterY(16);
leftButton.setRadiusX(15);
leftButton.setRadiusY(15);
leftButton.setStartAngle(-30);
leftButton.setLength(60);
leftButton.setFill(new Color(1, 1, 1, .90));

leftButton.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent me) -> {
int indx = gotoImageIndex(ButtonMove.PREV);
if (indx > -1) {
String namePict = imageFiles.get(indx);
namePict = filePrefix + namePict;
final Image image = new Image(namePict);
currentImageView.setImage(image);
}
});
buttonGroup.getChildren().add(leftButton);

// right control
Arc rightButton = new Arc();
rightButton.setType(ArcType.ROUND);
rightButton.setCenterX(12);
rightButton.setCenterY(16);
rightButton.setRadiusX(15);
rightButton.setRadiusY(15);
rightButton.setStartAngle(180 - 30);
rightButton.setLength(60);
rightButton.setFill(new Color(1, 1, 1, .90));
rightButton.setTranslateX(40);
buttonGroup.getChildren().add(rightButton);

rightButton.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent me) -> {
int indx = gotoImageIndex(ButtonMove.NEXT);
if (indx > -1) {
String namePict = imageFiles.get(indx);
namePict = filePrefix + namePict;
final Image image = new Image(namePict);
currentImageView.setImage(image);
}
});

// move button group when scene is resized
buttonGroup.translateXProperty().bind(scene.widthProperty().subtract(buttonArea.getWidth() + 6));
buttonGroup.translateYProperty().bind(scene.heightProperty().subtract(buttonArea.getHeight() + 6));
root.getChildren().add(buttonGroup);

primaryStage.setScene(scene);
primaryStage.show();
}

/**
* Returns the next index in the list of files to go to next.
*
* @param direction PREV and NEXT to move backward or forward in the list of
* pictures.
* @return int the index to the previous or next picture to be shown.
*/
public int gotoImageIndex(ButtonMove direction) {
int size = imageFiles.size();
if (size == 0) {
currentIndex = -1;
} else if (direction == ButtonMove.NEXT && size > 1 && currentIndex < size - 1) {
currentIndex += 1;
} else if (direction == ButtonMove.PREV && size > 1 && currentIndex > 0) {
currentIndex -= 1;
}

return currentIndex;
}

Figure 15-2 depicts the drag-and-drop operation that gives the user visual feedback with a thumbnail-sized image over the surface. In the figure, I’m dragging the image onto the application window.

9781430268277_Fig15-02.jpg

Figure 15-2. Drag-and-drop in progress

Figure 15-3 shows that the drop operation has succesfully loaded the image.

9781430268277_Fig15-03.jpg

Figure 15-3. Drop operation completed

How It Works

This recipe is a simple application that allows you to view images having file formats such as .jpg, .png, and .gif. Loading an image requires using the mouse to drag-and-drop a file onto the window area. The application also allows you to resize the window, which automatically causes the image to scale while maintaining its aspect ratio. After a few images are successfully loaded, you will be able to page through each image conveniently by clicking the left and right button controls, as shown in Figure 15-3.

Before the code walk-through, let’s discuss the application’s variables. Table 15-1 describes instance variables for this sleek image viewer application.

Table 15-1. The CreatingImages Instance Variables

Table15-1.jpg

When you’re dragging an image into the application, the imageFiles variable will cache the absolute file path as a string instead of as the actual image file in order to save memory space. If a user drags the same image file into the display area, the list will contain duplicate strings representing the image file. As an image is being displayed, the currentIndex variable contains the index into the imageFiles list. The imageFiles list points to the string representing the current image file. As the user clicks the buttons to display the previous and next image, thecurrentIndex will decrement or increment, respectively. Next, let’s walk through the code detailing the steps for loading and displaying an image. Later, you will learn the steps for paging through each image with the next and previous buttons.

Begin by instantiating an instance of the javafx.scene.image.ImageView class. The ImageView class is a graph node (Node) used to display an already loaded javafx.scene.image.Image object. Using the ImageView node will enable you to create special effects on the image to be displayed without manipulating the physical image. To avoid performance degradation when rendering many effects, you can use numerous ImageView objects that reference a single Image object. Many types of effects include blurring, fading, and transforming an image.

One of the requirements is preserving the displayed image’s aspect ratio as the user resizes the window. Here, you will simply call the setPreserveRatio() method with a value of true to preserve the image’s aspect ratio. Remember that because the user resizes the window, you want to bind the width of the ImageView to the Scene’s width to allow the image to be scaled. After setting up the ImageView, you will want to pass it to an HBox instance (pictureRegion) to be put into the scene. The following code creates the ImageView instance, preserves the aspect ratio, and scales the image:

// image view
final ImageView currentImageView = new ImageView();

// maintain aspect ratio
currentImageView.setPreserveRatio(true);

// resize based on the scene
currentImageView.fitWidthProperty().bind(scene.widthProperty());

Next, let’s cover JavaFX’s native drag-and-drop support, which provides many options for users, such as dragging visual objects from an application to be dropped into another application. In this scenario, the user will be dragging an image file from the host windowing operating system to the image viewer application. In this scenario, EventHandler objects must be generated to listen to DragEvents. To fulfill this requirement, you’ll set up a scene’s drag-over and drag-dropped event handler methods.

To set up the drag-over attribute, call the scene’s setOnDragOver() method with the appropriate generic EventHandler<DragEvent> type. In the example, a lambda expression is used to implement the event handler. Implement the handle() method via the lambda expression to listen to the drag-over event (DragEvent). In the event handler, notice the event (DragEvent) object’s invocation to the getDragboard() method. The call to getDragboard() will return the drag source (Dragboard), better known as the clipboard. Once theDragboard object is obtained, it is possible to determine and validate what is being dragged over the surface. In this scenario, you need to determine whether the Dragboard object contains any files. If it does, you call the event object’s acceptTransferModes() by passing in the constant TransferMode.COPY to provide visual feedback to the user of the application (refer to Figure 15-2). Otherwise, it should consume the event by calling the event.consume() method. The following code demonstrates setting up a scene’s OnDragOver attribute:

// Dragging over surface
scene.setOnDragOver((DragEvent event) -> {
Dragboard db = event.getDragboard();
if (db.hasFiles()) {
event.acceptTransferModes(TransferMode.COPY);
} else {
event.consume();
}
});

Once the drag-over event handler attribute is set, you create a drag-dropped event handler attribute so it can finalize the operation. Listening to a drag-dropped event is similar to listening to a drag-over event in which the handle() method will be implemented via a lambda expression. Once again, you obtain the Dragboard object from the event to determine whether the clipboard contains any files. If it does, the list of files is iterated and the file names are added to the imageFiles list. This code demonstrates setting up a scene’s OnDragDropped attribute:

// Dropping over surface
scene.setOnDragDropped((DragEvent event) -> {
Dragboard db = event.getDragboard();
boolean success = false;
if (db.hasFiles()) {
success = true;
String filePath = null;
for (File file : db.getFiles()) {
filePath = file.getAbsolutePath();
System.out.println(filePath);
currentIndex += 1;
imageFiles.add(currentIndex, filePath);
}
filePath = filePrefix + filePath;
// set new image as the image to show.
Image imageimage = new Image(filePath);
currentImageView.setImage(imageimage);

}
event.setDropCompleted(success);
event.consume();

});

As the last file is determined, the current image is displayed. The following code demonstrates loading an image to be displayed:

// set new image as the image to show.
Image imageimage = new Image(filePath);
currentImageView.setImage(imageimage);

For the last requirements relating to the image viewer application, simple controls are generated that allow the users to view the next or previous image. I emphasize “simple” controls because JavaFX contains two other methods for creating custom controls. One way (CSS styling) is discussed later, in Recipe 15-5. To explore the other alternative, refer to the Javadoc on the Skin and Skinnable APIs.

The simple buttons in this example are created using Java FX’s javafx.scene.shape.Arc to build the left and right arrows on top of a small transparent rounded rectangle called javafx.scene.shape.Rectangle. Next, an EventHandler that listens to mouse-pressed events is added via a lambda expression, and it will load and display the appropriate image based on the enums ButtonMove.PREV and ButtonMove.NEXT.

When instantiating a generic class with a type variable between the < and > symbols, the same type variable will be defined in the handle()’s signature. When implementing the event handler logic, you determine which button was pressed and then return the index into theimageFiles list of the next image to display. When loading an image using the Image class, it is possible to load images from the file system or from a URL. The following code instantiates an EventHandler<MouseEvent> lambda expression to display the previous image in theimageFiles list:

leftButton.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent me) -> {
int indx = gotoImageIndex(ButtonMove.PREV);
if (indx > -1) {
String namePict = imageFiles.get(indx);
namePict = filePrefix + namePict;
final Image image = new Image(namePict);
currentImageView.setImage(image);
}
});

The right button’s (rightButton) event handler is identical. The only thing different is that it must determine whether the previous or next button was pressed via the ButtonMove enum. This information is passed to the gotoImageIndex() method to determine whether an image is available in that direction.

To finish the image viewer application, you bind the rectangular button’s control to the scene’s width and height, which repositions the control as the user resizes the window. Here, you bind the translateXProperty() to the scene’s width property by subtracting thebuttonArea's width (Fluent API). In the example, you also bind the translateYProperty() based on the scene’s height property. Once your buttons control is bound, your user will experience user interface goodness. The following code uses the Fluent API to bind the button control’s properties to the scene’s properties:

// move button group when scene is resized
buttonGroup.translateXProperty().bind(scene.widthProperty().subtract(buttonArea.getWidth() + 6));
buttonGroup.translateYProperty().bind(scene.heightProperty().subtract(buttonArea.getHeight() + 6));
root.getChildren().add(buttonGroup);

15-2. Generating an Animation

Problem

You want to generate an animation. For example, you want to create a news ticker and photo viewer application with the following requirements:

· It will have a news ticker control that scrolls to the left.

· It will fade out the current picture and fade in the next picture as the user clicks the button controls.

· It will fade in and out button controls when the cursor moves in and out of the scene area, respectively.

· The news ticker will pause when the mouse hovers over the text, and will start again once the mouse moves away from the text.

Solution

Create animated effects by accessing JavaFX’s animation APIs (javafx.animation.*). To create a news ticker, you need the following classes:

· javafx.animation.TranslateTransition

· javafx.util.Duration

· javafx.event.EventHandler<ActionEvent>

· javafx.scene.shape.Rectangle

To fade out the current picture and fade in next picture, you need the following classes:

· javafx.animation.SequentialTransition

· javafx.animation.FadeTransition

· javafx.event.EventHandler<ActionEvent>

· javafx.scene.image.Image

· javafx.scene.image.ImageView

· javafx.util.Duration

To fade in and out button controls when the cursor moves into and out of the scene area, respectively, you need the following classes:

· javafx.animation.FadeTransition

· javafx.util.Duration

Shown here is the code used to create a news ticker control:

// create ticker area
final Group tickerArea = new Group();
final Rectangle tickerRect = new Rectangle();
tickerRect.setArcWidth(15);
tickerRect.setArcHeight(20);
tickerRect.setFill(new Color(0, 0, 0, .55));
tickerRect.setX(0);
tickerRect.setY(0);
tickerRect.setWidth(scene.getWidth() - 6);
tickerRect.setHeight(30);
tickerRect.setStroke(Color.rgb(255, 255, 255, .70));

Rectangle clipRegion = new Rectangle();
clipRegion.setArcWidth(15);
clipRegion.setArcHeight(20);
clipRegion.setX(0);
clipRegion.setY(0);
clipRegion.setWidth(scene.getWidth() - 6);
clipRegion.setHeight(30);
clipRegion.setStroke(Color.rgb(255, 255, 255, .70));

tickerArea.setClip(clipRegion);

// Resize the ticker area when the window is resized
tickerArea.setTranslateX(6);
tickerArea.translateYProperty().bind(scene.heightProperty().subtract(
tickerRect.getHeight() + 6));
tickerRect.widthProperty().bind(scene.widthProperty().subtract(
buttonRect.getWidth() + 16));
clipRegion.widthProperty().bind(scene.widthProperty().subtract(
buttonRect.getWidth() + 16));
tickerArea.getChildren().add(tickerRect);

root.getChildren().add(tickerArea);

// add news text
Text news = new Text();
news.setText("JavaFX 8 News Ticker... | New Features: Swing Node, Event Dispatch Thread and JavaFX
Application Thread Merge, " +
"New Look and Feel - Modena, Rich Text Support, Printing, Tree Table Control, Much More!");
news.setTranslateY(18);
news.setFill(Color.WHITE);
tickerArea.getChildren().add(news);

final TranslateTransition ticker = new TranslateTransition();
ticker.setNode(news);
int newsLength = news.getText().length();

// Calculated guess based upon length of text
ticker.setDuration(Duration.millis((newsLength * 4/300) * 15000));
ticker.setFromX(scene.widthProperty().doubleValue());
ticker.setToX(-scene.widthProperty().doubleValue() - (newsLength * 5));
ticker.setFromY(19);
ticker.setInterpolator(Interpolator.LINEAR);
ticker.setCycleCount(1);

// when ticker has finished reset and replay ticker animation
ticker.setOnFinished((ActionEvent ae) -> {
ticker.stop();
ticker.setFromX(scene.getWidth());
ticker.setDuration(new Duration((newsLength * 4/300) * 15000));
ticker.playFromStart();
});

// stop ticker if hovered over
tickerArea.setOnMouseEntered((MouseEvent me) -> {
ticker.pause();
});

// restart ticker if mouse leaves the ticker
tickerArea.setOnMouseExited((MouseEvent me) -> {
ticker.play();
});

ticker.play();

Here is the code used to fade out the current picture and fade in the next picture:

// previous button
Arc prevButton = // create arc ...

prevButton.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent me) -> {
int indx = gotoImageIndex(PREV);
if (indx > -1) {
String namePict = imagesFiles.get(indx);
final Image nextImage = new Image(namePict);
SequentialTransition seqTransition = transitionByFading(nextImage, currentImageView);
seqTransition.play();
}
});

buttonGroup.getChildren().add(prevButton);

// next button
Arc nextButton = //... create arc

buttonGroup.getChildren().add(nextButton);

nextButton.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent me) -> {
int indx = gotoImageIndex(NEXT);
if (indx > -1) {
String namePict = imagesFiles.get(indx);
final Image nextImage = new Image(namePict);
SequentialTransition seqTransition = transitionByFading(nextImage, currentImageView);
seqTransition.play();

}
});

//... the rest of the start(Stage primaryStage) method

public int gotoImageIndex(int direction) {
int size = imagesFiles.size();
if (size == 0) {
currentIndexImageFile = -1;
} else if (direction == NEXT && size > 1 && currentIndexImageFile < size - 1) {
currentIndexImageFile += 1;
} else if (direction == PREV && size > 1 && currentIndexImageFile > 0) {
currentIndexImageFile -= 1;
}

return currentIndexImageFile;
}

public SequentialTransition transitionByFading(final Image nextImage, final ImageView imageView) {
FadeTransition fadeOut = new FadeTransition(Duration.millis(500), imageView);
fadeOut.setFromValue(1.0);
fadeOut.setToValue(0.0);
fadeOut.setOnFinished((ActionEvent ae) -> {
imageView.setImage(nextImage);
});
FadeTransition fadeIn = new FadeTransition(Duration.millis(500), imageView);
fadeIn.setFromValue(0.0);
fadeIn.setToValue(1.0);
SequentialTransition seqTransition = new SequentialTransition();
seqTransition.getChildren().addAll(fadeOut, fadeIn);
return seqTransition;
}

The following code is used to fade in and out the button controls when the cursor moves into and out of the scene area, respectively:

// Fade in button controls
scene.setOnMouseEntered((MouseEvent me) -> {
FadeTransition fadeButtons = new FadeTransition(Duration.millis(500), buttonGroup);
fadeButtons.setFromValue(0.0);
fadeButtons.setToValue(1.0);
fadeButtons.play();
});
// Fade out button controls
scene.setOnMouseExited((MouseEvent me) -> {
FadeTransition fadeButtons = new FadeTransition(Duration.millis(500), buttonGroup);
fadeButtons.setFromValue(1);
fadeButtons.setToValue(0);
fadeButtons.play();
});

Figure 15-4 shows the photo viewer application with a ticker control in the bottom region of the screen.

9781430268277_Fig15-04.jpg

Figure 15-4. Photo viewer with a news ticker

How It Works

This recipe takes the photo viewer application from Recipe 15-1 and adds a news ticker and some nice photo-changing animation. The main animation effects focus on translating and fading. First, a news ticker control is created, and it scrolls Text nodes to the left by using a translation transition (javafx.animation.TranslateTransition). Next, another fading effect is applied so that slow transitions will occur when the user clicks the previous and next buttons to transition to the next image. To perform this effect, a compound transition (javafx.animation.SequentialTransition) is used, consisting of multiple animations. Finally, to create the effect of the button controls fading in and out based on where the mouse is located, you use a fade transition (javafx.animation.FadeTransition).

Before I begin to discuss the steps to fulfill the requirements, I want to mention the basics of JavaFX animation. The JavaFX animation API allows you to assemble timed events that can interpolate over a node’s attribute values to produce animated effects. Each timed event is called a keyframe (KeyFrame), and it’s responsible for interpolating over a node’s property over a period of time (javafx.util.Duration). Knowing that a keyframe’s job is to operate on a node’s property value, you have to create an instance of a KeyValue class that will reference the desired node property. The idea of interpolation is simply the distributing of values between a start and end value. An example is to move a rectangle by its current x position (zero) to 100 pixels in 1,000 milliseconds; in other words, move the rectangle 100 pixels to the right during one second. Shown here is a keyframe and key value to interpolate a rectangle’s x property for 1,000 milliseconds:

final Rectangle rectangle = new Rectangle(0, 0, 50, 50);
KeyValue keyValue = new KeyValue(rectangle.xProperty(), 100);
KeyFrame keyFrame = new KeyFrame(Duration.millis(1000), keyValue);

When creating many keyframes that are assembled consecutively, you need to create a timeline. Because timeline is a subclass of javafx.animation.Animation, there are standard attributes—such as its cycle count and auto-reverse—that you can set. The cycle count is the number of times you want the timeline to play the animation. If you want the cycle count to play the animation indefinitely, use the value Timeline.INDEFINITE. The auto-reverse is the capability for the animation to play the timeline backward. By default, the cycle count is set to 1, and the auto-reverse is set to false. When adding keyframes you simply add them using the getKeyFrames().add() method on the TimeLine object. The following code snippet demonstrates a timeline playing indefinitely with auto-reverse set to true:

Timeline timeline = new Timeline();
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.setAutoReverse(true);
timeline.getKeyFrames().add(keyFrame);
timeline.play();

With this knowledge of timelines you can animate any graph node in JavaFX. Although you can create timelines in a low-level way, it can become very cumbersome. You are probably wondering whether there are easier ways to express common animations. Good news! JavaFX has transitions (Transition), which are convenience classes that perform common animated effects. Some of the common animation effects you can create with transitions include:

· javafx.animation.FadeTransition

· javafx.animation.PathTransition

· javafx.animation.ScaleTransition

· javafx.animation.TranslateTransition

To see more transitions, see javafx.animation in the Javadoc. Because Transition objects are also subclasses of the javafx.animation.Animation class, you can set the cycle count and auto-reverse attributes. This recipe focuses on two transition effects: translate transition (TranslateTransition) and fade transition (FadeTransition).

The first requirement in the problem statement is to create a news ticker. In a news ticker control, Text nodes scroll from right to left inside a rectangular region. When the text scrolls to the left edge of the rectangular region you will want the text to be clipped to create a view port that only shows pixels inside of the rectangle. To do this, you first create a Group to hold all the components that comprise a ticker control. Next you create a white rounded rectangle filled with 55 percent opacity. After creating the visual region, you create a similar rectangle that represents the clipped region using the setClip(someRectangle) method on the Group object. Figure 15-5 shows a rounded rectangular area that serves as the clipped region.

9781430268277_Fig15-05.jpg

Figure 15-5. Setting the clipped region on the Group object

Once the ticker control is created, you bind the translate Y based on the scene’s height property minus the ticker control’s height. You also bind the ticker control’s width property based on the width of scene minus the button control’s width. By binding these properties, the ticker control can change its size and position whenever a user resizes the application window. This makes the ticker control appear to float at the bottom of the window. The following code binds the ticker control’s translate Y, width, and clip region’s width property:

tickerArea.translateYProperty().bind(scene.heightProperty().subtract(tickerRect.getHeight() + 6));
tickerRect.widthProperty().bind(scene.widthProperty().subtract(buttonRect.getWidth() + 16));
clipRegion.widthProperty().bind(scene.widthProperty().subtract(buttonRect.getWidth() + 16));
tickerArea.getChildren().add(tickerRect);

Now that the ticker control is complete, you’ll create some news to feed into it. In the example, a Text node with text that represents a news feed is used. To add a newly created Text node to the ticker control, you call its getChildren().add() method. The following code adds aText node to the ticker control:

final Group tickerArea = new Group();
final Rectangle tickerRect = //...
Text news = new Text();
news.setText("JavaFX 8 News Ticker... | New Features: Swing Node, Event Dispatch Thread and JavaFX
Application Thread Merge, " +
"New Look and Feel - Modena, Rich Text Support, Printing, Tree Table Control, Much More!");
news.setTranslateY(18);
news.setFill(Color.WHITE);
tickerArea.getChildren().add(news);

Next you have to scroll the Text node from right to left using JavaFX’s TranslateTransition API. The first step is to set the target node to perform the TranslateTransition. Then you set the duration, which is the total amount of time the TranslateTransition will spend animating. A TranslateTransition simplifies the creation of an animation by exposing convenience methods that operate on a Node’s translate X and Y properties. The convenience methods are prepended with from and to. For instance, in the scenario in which you use translate X on a Text node, there are the methods fromX() and toX(). The fromX() is the starting value and the toX() is the end value that will be interpolated. In the example, you base these calculations on the length of the text in the Text node. Therefore, if you are reading from a remote source, such as an RSS feed, the text length difference should not break the ticker. Next, you set the TranslateTransition to a linear transition (Interpolator.LINEAR) to interpolate evenly between the start and end values. To see more interpolator types or to see how to create custom interpolators, see the Javadoc on javafx.animation.Interpolators. Finally, in the example the cycle count is set to 1, which will animate the ticker once based on the specified duration. The following code snippet details creating a TranslateTransition that animates a Text node from right to left:

final TranslateTransition ticker = new TranslateTransition();
ticker.setNode(news);
int newsLength = news.getText().length();
ticker.setDuration(Duration.millis((newsLength * 4/300) * 15000));
ticker.setFromX(scene.widthProperty().doubleValue());
ticker.setToX(-scene.widthProperty().doubleValue() - (newsLength * 5));
ticker.setFromY(19);
ticker.setInterpolator(Interpolator.LINEAR);
ticker.setCycleCount(1);

When the ticker’s news has scrolled completely off of the ticker area to the far left of the scene, you will want to stop and replay the news feed from the start (the far right). To do this, you create an instance of an EventHandler<ActionEvent> object via a lambda expression, to be set on the ticker (TranslateTransition) object using the setOnFinished() method. Here is how you replay the TranslateTransition animation:

// when window resizes width wise the ticker will know how far to move
// when ticker has finished reset and replay ticker animation
ticker.setOnFinished((ActionEvent ae) -> {
ticker.stop();
ticker.setFromX(scene.getWidth());
ticker.setDuration(new Duration((newsLength * 4/300) * 15000));
ticker.playFromStart();
});

Once the animation is defined, you simply invoke the play() method to get it started. The following code snippet shows how to play a TranslateTransition:

ticker.play();

To pause and start the ticker when the mouse hovers over and leaves the text, you need to implement similar event handlers:

// stop ticker if hovered over
tickerArea.setOnMouseEntered((MouseEvent me) -> {
ticker.pause();
});

// restart ticker if mouse leaves the ticker
tickerArea.setOnMouseExited((MouseEvent me) -> {
ticker.play();
});

Now that you have a better understanding of animated transitions, what about a transition that can trigger any number of transitions? JavaFX has two transitions that provide this behavior. The two transitions can invoke individual dependent transitions sequentially or in parallel. In this recipe, you’ll use a sequential transition (SequentialTransition) to contain two FadeTransitions in order to fade out the current image displayed and to fade in the next image. When creating the previous and next button’s event handlers, you first determine the next image to be displayed by calling the gotoImageIndex() method. Once the next image to be displayed is determined, you call the transitionByFading() method, which returns an instance of a SequentialTransition. When calling the transitionByFading() method, you’ll notice that two FadeTransitions are created. The first transition will change the opacity level from 1.0 to 0.0 to fade out the current image, and the second transition will interpolate the opacity level from 0.0 to 1.0, fading in the next image, which then becomes the current image. At last the two FadeTransitions are added to the SequentialTransition and returned to the caller. The following code creates two FadeTransitions and adds them to a SequentialTransition:

FadeTransition fadeOut = new FadeTransition(Duration.millis(500), imageView);
fadeOut.setFromValue(1.0);
fadeOut.setToValue(0.0);
fadeOut.setOnFinished((ActionEvent ae) -> {
imageView.setImage(nextImage);
});
FadeTransition fadeIn = new FadeTransition(Duration.millis(500), imageView);
fadeIn.setFromValue(0.0);
fadeIn.setToValue(1.0);
SequentialTransition seqTransition = new SequentialTransition();
seqTransition.getChildren().addAll(fadeOut, fadeIn);
return seqTransition;

For the last requirements relating to fading in and out, use the button controls. Use the FadeTransition to create a ghostly animated effect. For starters, you create an EventHandler (more specifically, an EventHandler<MouseEvent> via a lambda expression). It is easy to add mouse events to the scene; all you have to do is override the handle() method where the inbound parameter is a MouseEvent type (the same as its formal type parameter). Inside of the lambda, you create an instance of a FadeTransition object by using the constructor that takes the duration and node as parameters. Next, you’ll notice the setFromValue() and setToValue() methods that are called to interpolate values between 1.0 and 0.0 for the opacity level, causing the fade in effect to occur. The following code adds an EventHandler to create the fade in effect when the mouse cursor is positioned inside of the scene:

// Fade in button controls
scene.setOnMouseEntered((MouseEvent me) -> {
FadeTransition fadeButtons = new FadeTransition(Duration.millis(500), buttonGroup);
fadeButtons.setFromValue(0.0);
fadeButtons.setToValue(1.0);
fadeButtons.play();
});

Last but not least, the fade out EventHandler is basically the same as the fade in, except that the opacity From and To values are from 1.0 to 0.0, which make the buttons vanish mysteriously when the mouse pointer moves off the scene area.

15-3. Animating Shapes Along a Path

Problem

You want to create a way to animate shapes along a path.

Solution

Create an application that allows users to draw the path for a shape to follow. The main Java classes used in this recipe are these:

· javafx.animation.PathTransition

· javafx.scene.input.MouseEvent

· javafx.event.EventHandler

· javafx.geometry.Point2D

· javafx.scene.shape.LineTo

· javafx.scene.shape.MoveTo

· javafx.scene.shape.Path

The following code demonstrates drawing a path for a shape to follow:

package org.java8recipes.chapter15.recipe15_03;

import javafx.animation.PathTransition;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
import javafx.util.Duration;

/**
* Recipe 15-3: Working with the Scene Graph
* @author cdea
* Update: J Juneau
*/
public class WorkingWithTheSceneGraph extends Application {
Path onePath = new Path();
Point2D anchorPt;
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Chapter 15-3 Working with the Scene Graph");
final Group root = new Group();
// add path
root.getChildren().add(onePath);
final Scene scene = new Scene(root, 300, 250);
scene.setFill(Color.WHITE);
RadialGradient gradient1 = new RadialGradient(0,
.1,
100,
100,
20,
false,
CycleMethod.NO_CYCLE,
new Stop(0, Color.RED),
new Stop(1, Color.BLACK));
// create a sphere
final Circle sphere = new Circle();
sphere.setCenterX(100);
sphere.setCenterY(100);
sphere.setRadius(20);
sphere.setFill(gradient1);

// add sphere
root.getChildren().add(sphere);

// animate sphere by following the path.
final PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.millis(4000));
pathTransition.setCycleCount(1);
pathTransition.setNode(sphere);
pathTransition.setPath(onePath);
pathTransition.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);

// once finished clear path
pathTransition.onFinishedProperty().set((EventHandler<ActionEvent>)
(ActionEvent event) -> {
onePath.getElements().clear();
});

// starting initial path
scene.onMousePressedProperty().set((EventHandler<MouseEvent>)
(MouseEvent event) -> {
onePath.getElements().clear();
// start point in path
anchorPt = new Point2D(event.getX(), event.getY());
onePath.setStrokeWidth(3);
onePath.setStroke(Color.BLACK);
onePath.getElements().add(new MoveTo(anchorPt.getX(), anchorPt.getY()));
});
// dragging creates lineTos added to the path
scene.onMouseDraggedProperty().set((EventHandler<MouseEvent>)
(MouseEvent event) -> {
onePath.getElements().add(new LineTo(event.getX(), event.getY()));
});
// end the path when mouse released event
scene.onMouseReleasedProperty().set((EventHandler<MouseEvent>)
(MouseEvent event) -> {
onePath.setStrokeWidth(0);
if (onePath.getElements().size() > 1) {
pathTransition.stop();
pathTransition.playFromStart();
}
});

primaryStage.setScene(scene);
primaryStage.show();
}
}

Figure 15-6 shows the drawn path the circle will follow. When the user performs a mouse release, the drawn path will disappear and the red ball will follow the path drawn earlier.

9781430268277_Fig15-06.jpg

Figure 15-6. Path transition

How It Works

In this recipe, you create a simple application enabling objects to follow a drawn path on the scene graph. To make things simple, the example uses one shape (Circle) that performs a path transition (javafx.animation.PathTransition). The application user will draw a path on the scene surface by pressing the mouse button like a drawing program. Once satisfied with the path drawn, the user releases the mouse press, which triggers the red ball to follow the path, similar to objects moving through pipes inside a building.

You first create two instance variables to maintain the coordinates that make up the path. To hold the path being drawn, create an instance of a javafx.scene.shape.Path object. The path instance should be added to the scene graph before the start of the application. Shown here is the process of adding the instance variable onePath to the scene graph:

// add path
root.getChildren().add(onePath);

Next, you create an instance variable anchorPt (javafx.geometry.Point2D) that will hold the path’s starting point. Later, you will see how these variables are updated based on mouse events. Shown here are the instance variables that maintain the currently drawn path:

Path onePath = new Path();
Point2D anchorPt;

First, let’s create a shape that will be animated. In this scenario, you’ll create a cool-looking red ball. To create a spherical-looking ball, create a gradient color RadialGradient that’s used to paint or fill a circle shape. (Refer to Recipe 15-6 for how to fill shapes with a gradient paint.) Once you have created the red spherical ball, you need to create the PathTransition object to perform the path-following animation. After instantiating a PathTransition() object, simply set the duration to four seconds and the cycle count to one. The cycle count is the number of times the animation cycle will occur. Next, you set the node to reference the red ball (sphere). Then, you set the path() method to the instance variable onePath, which contains all the coordinates and lines that make up a drawn path. After setting the path for the sphere to animate, you should specify how the shape will follow the path, such as perpendicular to a tangent point on the path. The following code creates an instance of a path transition:

// animate sphere by following the path.
final PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.millis(4000));
pathTransition.setCycleCount(1);
pathTransition.setNode(sphere);
pathTransition.setPath(onePath);
pathTransition.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);

After you’ve created the path transition, you’ll want it to clean up when the animation is completed. To reset or clean up the path variable when the animation is finished, create and add an event handler to listen to the onFinished property event on the path transition object.

The following code snippet adds an event handler to clear the current path information:

// once finished clear path
pathTransition.onFinishedProperty().set((EventHandler<ActionEvent>)
(ActionEvent event) -> {
onePath.getElements().clear();
});

With the shape and transition all set up, the application needs to respond to mouse events that will update the instance variable mentioned earlier. To do so, listen to mouse events occurring on the Scene object. Here, you will once again rely on creating event handlers to be set on the scene’s onMouseXXXProperty methods, where the XXX denotes the actual mouse event name such as pressed, dragged, and released.

When a user draws a path, he or she will perform a mouse-press event to begin the start of the path. To listen to a mouse-press event, create an event handler with a formal type parameter of MouseEvent. In the example, a lambda expression is used. As a mouse-press event occurs, clear the instance variable onePath of any prior drawn path information. Next, simply set the stroke width and color of the path so the users can see the path being drawn. Finally, add the starting point to the path using an instance of a MoveTo object. Shown here is the handler code that responds when the user performs a mouse press:

// starting initial path
scene.onMousePressedProperty().set((EventHandler<MouseEvent>)
(MouseEvent event) -> {
onePath.getElements().clear();
// start point in path
anchorPt = new Point2D(event.getX(), event.getY());
onePath.setStrokeWidth(3);
onePath.setStroke(Color.BLACK);
onePath.getElements().add(new MoveTo(anchorPt.getX(), anchorPt.getY()));
});

Once the mouse-press event handler is in place, you create another handler for mouse-drag events. Again, look for the scene’s onMouseXXXProperty() methods that correspond to the proper mouse event that you care about. In this case, the onMouseDraggedProperty() will be set. Inside the lambda expression, obtain mouse coordinates that will be converted to LineTo objects to be added to the path (Path). These LineTo objects are instances of path element (javafx.scene.shape.PathElement), as discussed in Recipe 15-5. The following code is an event handler responsible for mouse-drag events:

// dragging creates lineTos added to the path
scene.onMouseDraggedProperty().set((EventHandler<MouseEvent>)
(MouseEvent event) -> {
onePath.getElements().add(new LineTo(event.getX(), event.getY()));
});

Finally, create an event handler to listen to a mouse-release event. When a user releases the mouse, the path’s stroke is set to zero to appear as if it has removed. Then you reset the path transition by stopping it and playing it from the start. The following code is an event handler responsible for a mouse-release event:

// end the path when mouse released event
scene.onMouseReleasedProperty().set((EventHandler<MouseEvent>)
(MouseEvent event) -> {
onePath.setStrokeWidth(0);
if (onePath.getElements().size() > 1) {
pathTransition.stop();
pathTransition.playFromStart();
}
});

15-4. Manipulating Layout via Grids

Problem

You want to create a nice-looking form-based user interface using a grid type layout.

Solution

Create a simple form designer application to manipulate the user interface dynamically using the JavaFX’s javafx.scene.layout.GridPane. The form designer application will have the following features:

· It will toggle the display of the grid layout’s grid lines for debugging.

· It will adjust the top padding of the GridPane.

· It will adjust the left padding of the GridPane.

· It will adjust the horizontal gap between cells in the GridPane.

· It will adjust the vertical gap between cells in the GridPane.

· It will align controls within cells horizontally.

· It will align controls within cells vertically.

The following code is the main launching point for the form designer application:

public class ManipulatingLayoutViaGrids extends Application {

/**
* @param args the command line arguments
*/
public static void main(String[] args) {
Application.launch(args);
}

@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Chapter 15-4 Manipulating Layout via Grids ");
Group root = new Group();
Scene scene = new Scene(root, 640, 480, Color.WHITE);

// Left and right split pane
SplitPane splitPane = new SplitPane();
splitPane.prefWidthProperty().bind(scene.widthProperty());
splitPane.prefHeightProperty().bind(scene.heightProperty());

// Form on the right
GridPane rightGridPane = new MyForm();

GridPane leftGridPane = new GridPaneControlPanel(rightGridPane);

VBox leftArea = new VBox(10);

leftArea.getChildren().add(leftGridPane);
HBox hbox = new HBox();
hbox.getChildren().add(splitPane);
root.getChildren().add(hbox);
splitPane.getItems().addAll(leftArea, rightGridPane);

primaryStage.setScene(scene);

primaryStage.show();
}
}

When the form designer application is launched, the target form to be manipulated is shown on the right side of the window’s split pane. The following code is a simple grid-like form class that extends from GridPane. It will be manipulated by the form designer application:

/**
* MyForm is a form to be manipulated by the user.
* @author cdea
*/
public class MyForm extends GridPane{
public MyForm() {

setPadding(new Insets(5));
setHgap(5);
setVgap(5);

Label fNameLbl = new Label("First Name");
TextField fNameFld = new TextField();
Label lNameLbl = new Label("Last Name");
TextField lNameFld = new TextField();
Label ageLbl = new Label("Age");
TextField ageFld = new TextField();

Button saveButt = new Button("Save");

// First name label
GridPane.setHalignment(fNameLbl, HPos.RIGHT);
add(fNameLbl, 0, 0);

// Last name label
GridPane.setHalignment(lNameLbl, HPos.RIGHT);
add(lNameLbl, 0, 1);

// Age label
GridPane.setHalignment(ageLbl, HPos.RIGHT);
add(ageLbl, 0, 2);

// First name field
GridPane.setHalignment(fNameFld, HPos.LEFT);
add(fNameFld, 1, 0);

// Last name field
GridPane.setHalignment(lNameFld, HPos.LEFT);
add(lNameFld, 1, 1);

// Age Field
GridPane.setHalignment(ageFld, HPos.RIGHT);
add(ageFld, 1, 2);

// Save button
GridPane.setHalignment(saveButt, HPos.RIGHT);
add(saveButt, 1, 3);

}
}

When the form designer application is launched, the grid property control panel is shown on the left side of the window’s split pane. The property control panel allows the users to manipulate the target form’s grid pane attributes dynamically. The following code represents the grid property control panel that will manipulate a target grid pane’s properties:

/** GridPaneControlPanel represents the left area of the split pane
* allowing the user to manipulate the GridPane on the right.
*
* Manipulating Layout Via Grids
* @author cdea
*/
public class GridPaneControlPanel extends GridPane{
public GridPaneControlPanel(final GridPane targetGridPane) {
super();

setPadding(new Insets(5));
setHgap(5);
setVgap(5);

// Setting Grid lines
Label gridLinesLbl = new Label("Grid Lines");
final ToggleButton gridLinesToggle = new ToggleButton("Off");
gridLinesToggle.selectedProperty().addListener((ObservableValue<? extends Boolean> ov,
Boolean oldValue, Boolean newVal) -> {
targetGridPane.setGridLinesVisible(newVal);
gridLinesToggle.setText(newVal ? "On" : "Off");
});

// toggle grid lines label
GridPane.setHalignment(gridLinesLbl, HPos.RIGHT);
add(gridLinesLbl, 0, 0);

// toggle grid lines
GridPane.setHalignment(gridLinesToggle, HPos.LEFT);
add(gridLinesToggle, 1, 0);

// Setting padding [top]
Label gridPaddingLbl = new Label("Top Padding");

final Slider gridPaddingSlider = new Slider();
gridPaddingSlider.setMin(0);
gridPaddingSlider.setMax(100);
gridPaddingSlider.setValue(5);
gridPaddingSlider.setShowTickLabels(true);
gridPaddingSlider.setShowTickMarks(true);
gridPaddingSlider.setMinorTickCount(1);
gridPaddingSlider.setBlockIncrement(5);

gridPaddingSlider.valueProperty().addListener((ObservableValue<? extends Number> ov,
Number oldVal, Number newVal) -> {
double top1 = targetGridPane.getInsets().getTop();
double right1 = targetGridPane.getInsets().getRight();
double bottom1 = targetGridPane.getInsets().getBottom();
double left1 = targetGridPane.getInsets().getLeft();
Insets newInsets = new Insets((double) newVal, right1, bottom1, left1);
targetGridPane.setPadding(newInsets);
});

// padding adjustment label
GridPane.setHalignment(gridPaddingLbl, HPos.RIGHT);
add(gridPaddingLbl, 0, 1);

// padding adjustment slider
GridPane.setHalignment(gridPaddingSlider, HPos.LEFT);
add(gridPaddingSlider, 1, 1);

// Setting padding [top]
Label gridPaddingLeftLbl = new Label("Left Padding");

final Slider gridPaddingLeftSlider = new Slider();
gridPaddingLeftSlider.setMin(0);
gridPaddingLeftSlider.setMax(100);
gridPaddingLeftSlider.setValue(5);
gridPaddingLeftSlider.setShowTickLabels(true);
gridPaddingLeftSlider.setShowTickMarks(true);
gridPaddingLeftSlider.setMinorTickCount(1);
gridPaddingLeftSlider.setBlockIncrement(5);

gridPaddingLeftSlider.valueProperty().addListener((ObservableValue<? extends Number> ov,
Number oldVal, Number newVal) -> {
double top1 = targetGridPane.getInsets().getTop();
double right1 = targetGridPane.getInsets().getRight();
double bottom1 = targetGridPane.getInsets().getBottom();
double left1 = targetGridPane.getInsets().getLeft();
Insets newInsets = new Insets(top1, right1, bottom1, (double) newVal);
targetGridPane.setPadding(newInsets);
});

// padding adjustment label
GridPane.setHalignment(gridPaddingLeftLbl, HPos.RIGHT);
add(gridPaddingLeftLbl, 0, 2);

// padding adjustment slider
GridPane.setHalignment(gridPaddingLeftSlider, HPos.LEFT);
add(gridPaddingLeftSlider, 1, 2);

// Horizontal gap
Label gridHGapLbl = new Label("Horizontal Gap");

final Slider gridHGapSlider = new Slider();
gridHGapSlider.setMin(0);
gridHGapSlider.setMax(100);
gridHGapSlider.setValue(5);
gridHGapSlider.setShowTickLabels(true);
gridHGapSlider.setShowTickMarks(true);
gridHGapSlider.setMinorTickCount(1);
gridHGapSlider.setBlockIncrement(5);

gridHGapSlider.valueProperty().addListener((ObservableValue<? extends Number> ov,
Number oldVal, Number newVal) -> {
targetGridPane.setHgap((double) newVal);
});

// hgap label
GridPane.setHalignment(gridHGapLbl, HPos.RIGHT);
add(gridHGapLbl, 0, 3);

// hgap slider
GridPane.setHalignment(gridHGapSlider, HPos.LEFT);
add(gridHGapSlider, 1, 3);

// Vertical gap
Label gridVGapLbl = new Label("Vertical Gap");

final Slider gridVGapSlider = new Slider();
gridVGapSlider.setMin(0);
gridVGapSlider.setMax(100);
gridVGapSlider.setValue(5);
gridVGapSlider.setShowTickLabels(true);
gridVGapSlider.setShowTickMarks(true);
gridVGapSlider.setMinorTickCount(1);
gridVGapSlider.setBlockIncrement(5);

gridVGapSlider.valueProperty().addListener((ObservableValue<? extends Number> ov,
Number oldVal, Number newVal) -> {
targetGridPane.setVgap((double) newVal);
});

// vgap label
GridPane.setHalignment(gridVGapLbl, HPos.RIGHT);
add(gridVGapLbl, 0, 4);

// vgap slider
GridPane.setHalignment(gridVGapSlider, HPos.LEFT);
add(gridVGapSlider, 1, 4);

// Cell Column
Label cellCol = new Label("Cell Column");
final TextField cellColFld = new TextField("0");

// cell Column label
GridPane.setHalignment(cellCol, HPos.RIGHT);
add(cellCol, 0, 5);

// cell Column field
GridPane.setHalignment(cellColFld, HPos.LEFT);
add(cellColFld, 1, 5);

// Cell Row
Label cellRowLbl = new Label("Cell Row");
final TextField cellRowFld = new TextField("0");

// cell Row label
GridPane.setHalignment(cellRowLbl, HPos.RIGHT);
add(cellRowLbl, 0, 6);

// cell Row field
GridPane.setHalignment(cellRowFld, HPos.LEFT);
add(cellRowFld, 1, 6);

// Horizontal Alignment
Label hAlignLbl = new Label("Horiz. Align");
final ChoiceBox hAlignFld = new ChoiceBox(FXCollections.observableArrayList(
"CENTER", "LEFT", "RIGHT")
);
hAlignFld.getSelectionModel().select("LEFT");

// cell Row label
GridPane.setHalignment(hAlignLbl, HPos.RIGHT);
add(hAlignLbl, 0, 7);

// cell Row field
GridPane.setHalignment(hAlignFld, HPos.LEFT);
add(hAlignFld, 1, 7);

// Vertical Alignment
Label vAlignLbl = new Label("Vert. Align");
final ChoiceBox vAlignFld = new ChoiceBox(FXCollections.observableArrayList(
"BASELINE", "BOTTOM", "CENTER", "TOP")
);
vAlignFld.getSelectionModel().select("TOP");
// cell Row label
GridPane.setHalignment(vAlignLbl, HPos.RIGHT);
add(vAlignLbl, 0, 8);

// cell Row field
GridPane.setHalignment(vAlignFld, HPos.LEFT);
add(vAlignFld, 1, 8);

// Vertical Alignment
Label cellApplyLbl = new Label("Cell Constraint");
final Button cellApplyButton = new Button("Apply");
cellApplyButton.setOnAction((ActionEvent event) -> {
for (Node child:targetGridPane.getChildren()) {

int targetColIndx = 0;
int targetRowIndx = 0;
try {
targetColIndx = Integer.parseInt(cellColFld.getText());
targetRowIndx = Integer.parseInt(cellRowFld.getText());
} catch (NumberFormatException e) {

}
System.out.println("child = " + child.getClass().getSimpleName());
int col = GridPane.getColumnIndex(child);
int row = GridPane.getRowIndex(child);
if (col == targetColIndx && row == targetRowIndx) {
GridPane.setHalignment(child, HPos.valueOf(hAlignFld.getSelectionModel().
getSelectedItem().toString()));
GridPane.setValignment(child, VPos.valueOf(vAlignFld.getSelectionModel().
getSelectedItem().toString()));
}
}
});

// cell Row label
GridPane.setHalignment(cellApplyLbl, HPos.RIGHT);
add(cellApplyLbl, 0, 9);

// cell Row field
GridPane.setHalignment(cellApplyButton, HPos.LEFT);
add(cellApplyButton, 1, 9);

}
}

Figure 15-7 shows a form designer application with the GridPane property control panel on the left and the target form on the right.

9781430268277_Fig15-07.jpg

Figure 15-7. Manipulating layout via grids

How It Works

The form designer application allows the users to adjust properties dynamically using the GridPane property control panel to the left. While adjusting properties from the left control panel, the target form on the right side will be manipulated dynamically. When creating a simple form designer application, you will be binding controls to various properties onto the target form (GridPane). This designer application is basically broken into three classes: ManipulatingLayoutViaGrids, MyForm, and GridPaneControlPanel. TheManipulatingLayoutViaGrids class is the main application to be launched. MyForm is the target form that will be manipulated, and GridPaneControlPanel is the grid property control panel that has UI controls bound to the targets form’s grid pane properties.

Begin by creating the main launching point for the application (ManipulatingLayoutViaGrids). This class is responsible for creating a split pane (SplitPane) that sets up the target form to the right and instantiates a GridPaneControlPanel to be displayed to the left. To instantiate a GridPaneControlPanel you must pass in the target form you want to manipulate into the constructor. I will discuss this further, but suffice it to say that the GridPaneControlPanel constructor will wire its controls to properties on the target form.

Next, you create a dummy form called MyForm. This is your target form that the property control panel will manipulate. Here, notice that the MyForm extends GridPane. In the MyForm’s constructor, you create and add controls to be put into the form (GridPane).

To learn more about the GridPane, refer to Recipe 15-8. The following code is a target form to be manipulated by the form designer application:

/**
* MyForm is a form to be manipulated by the user.
* @author cdea
*/
public class MyForm extends GridPane{
public MyForm() {

setPadding(new Insets(5));
setHgap(5);
setVgap(5);

Label fNameLbl = new Label("First Name");
TextField fNameFld = new TextField();
Label lNameLbl = new Label("Last Name");
TextField lNameFld = new TextField();
Label ageLbl = new Label("Age");
TextField ageFld = new TextField();

Button saveButt = new Button("Save");

// First name label
GridPane.setHalignment(fNameLbl, HPos.RIGHT);
add(fNameLbl, 0, 0);
//... The rest of the form code

To manipulate the target form you need to create a grid property control panel (GridPaneControlPanel). This class is responsible for binding the target form’s grid pane properties to UI controls that allow users to adjust values using the keyboard and mouse. As you learned inChapter 14, in Recipe 14-10, you can bind values with JavaFX properties. But instead of binding values directly, you can also be notified when a property has changed.

Another feature that you can add to properties is the change listener. JavaFX javafx.beans.value.ChangeListeners are similar to Java swing’s property change support (java.beans.PropertyChangeListener). Similarly, when a bean’s property value has changed, you will want to be notified. Change listeners are designed to intercept the change by making the old and new value available to the developer. The example starts this process by creating a JavaFXchange listener for the toggle button to turn gridlines on or off. When a user interacts with the toggle button, the change listener will simply update the target’s grid pane’s gridlinesVisible property. Because a toggle button’s (ToggleButton) selected property is a Boolean value, you instantiate a ChangeListener class with its formal type parameter as Boolean. You’ll also notice the lambda expression change listener implementation, where its inbound parameters will match the generic formal type parameter specified when instantiating a ChangeListener<Boolean>. When a property change event occurs, the change listener will invokesetGridLinesVisible() on the target grid pane with the new value and update the toggle button’s text. The following code snippet shows a ChangeListener<Boolean> added to a ToggleButton:

gridLinesToggle.selectedProperty().addListener(
(ObservableValue<? extends Boolean> ov,
Boolean oldValue, Boolean newVal) -> {
targetGridPane.setGridLinesVisible(newVal);
gridLinesToggle.setText(newVal ? "On" : "Off");
});

Next, you apply a change listener to a slider control that allows the user to adjust the target grid pane’s top padding. To create a change listener for a slider, you instantiate a ChangeListener<Number>. Again, you’ll use a lambda expression with a signature the same as its formal type parameter Number. When a change occurs, the slider’s value is used to create an Insets object, which becomes the new padding for the target grid pane. Shown here is the change listener for the top padding and slider control:

gridPaddingSlider.valueProperty().addListener((
ObservableValue<? extends Number> ov, Number oldVal, Number newVal) -> {
double top1 = targetGridPane.getInsets().getTop();
double right1 = targetGridPane.getInsets().getRight();
double bottom1 = targetGridPane.getInsets().getBottom();
double left1 = targetGridPane.getInsets().getLeft();
Insets newInsets = new Insets((double) newVal, right1, bottom1, left1);
targetGridPane.setPadding(newInsets);
});

Because the implementation of the other slider controls that handle left padding, horizontal gap, and vertical gap are virtually identical to the top padding slider control mentioned previously, you can fast-forward to cell constraints controls.

The last bits of grid control panel properties that you want to manipulate are the target grid pane’s cell constraints. For brevity, the example only allows the user to set a component’s alignment inside of a cell of a GridPane. To see more properties to modify, refer to the Javadoc onjavafx.scene.layout.GridPane. Figure 15-8 depicts the cell constraint settings for individual cells. An example is to left-justify the label Age on the target grid pane. Because cells are zero-relative, you will enter 0 in the Cell Column field and 2 into the Cell Row field. Next, you select the drop-down box Horiz. Align to LEFT. Once you’re satisfied with the settings, click Apply. Figure 15-9 shows the Age label control left-aligned horizontally. To implement this change, create a lambda expression that implements EventHandler<ActionEvent> for the apply button’s onAction attribute. Inside of the lambda expression, you iterate the node children owned by the target grid pane to determine whether it is the specified cell. Once the specified cell and child node is determined, the alignment is applied. The following code shows anEventHandler that applies a cell constraint when the apply button is pressed:

cellApplyButton.setOnAction((ActionEvent event) -> {
for (Node child:targetGridPane.getChildren()) {

int targetColIndx = 0;
int targetRowIndx = 0;
try {
targetColIndx = Integer.parseInt(cellColFld.getText());
targetRowIndx = Integer.parseInt(cellRowFld.getText());
} catch (NumberFormatException e) {

}
System.out.println("child = " + child.getClass().getSimpleName());
int col = GridPane.getColumnIndex(child);
int row = GridPane.getRowIndex(child);
if (col == targetColIndx && row == targetRowIndx) {
GridPane.setHalignment(child, HPos.valueOf(hAlignFld.getSelectionModel().
getSelectedItem().toString()));
GridPane.setValignment(child, VPos.valueOf(vAlignFld.getSelectionModel().
getSelectedItem().toString()));
}
}
});

Figure 15-8 depicts the cell constraint grid control panel section that left-aligns the control at cell column 0 and cell row 2.

9781430268277_Fig15-08.jpg

Figure 15-8. Cell constraints

Figure 15-9 depicts the target grid pane with the grid lines turned on and the Age label left-aligned horizontally at cell column 0 and cell row 2.

9781430268277_Fig15-09.jpg

Figure 15-9. Target grid pane

15-5. Enhancing the Interface with CSS

Problem

You want to change the Look and Feel of the GUI interface.

Solution

Apply JavaFX’s CSS styling to graph nodes. The following code demonstrates using CSS styling on graph nodes. The code creates five themes: Modena, Caspian, Control Style 1, Control Style 2, and Sky. Each theme is defined using CSS and affects the Look and Feel of a dialog box. Following the code, you can see the two different renditions of the dialog box:

package org.java8recipes.chapter15.recipe15_05;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/**
* Recipe 15-5: Enhancing with CSS
* @author cdea
* Update: J Juneau
*/
public class EnhancingWithCss extends Application {

/**
* @param args the command line arguments
*/
public static void main(String[] args) {
Application.launch(args);
}

@Override
public void start(Stage primaryStage) {

primaryStage.setTitle("Chapter 15-5 Enhancing with CSS ");
Group root = new Group();
final Scene scene = new Scene(root, 640, 480, Color.BLACK);
MenuBar menuBar = new MenuBar();
Menu menu = new Menu("Look and Feel");

// New Modena Look and Feel
MenuItem modenaLnf = new MenuItem("Modena");
modenaLnf.setOnAction(enableCss(STYLESHEET_MODENA,scene));
menu.getItems().add(modenaLnf);

// Old default, Caspian Look and Feel
MenuItem caspianLnf = new MenuItem("Caspian");
caspianLnf.setOnAction(enableCss(STYLESHEET_CASPIAN, scene));

menu.getItems().add(caspianLnf);

menu.getItems().add(createMenuItem("Control Style 1", "controlStyle1.css", scene));
menu.getItems().add(createMenuItem("Control Style 2", "controlStyle2.css", scene));
menu.getItems().add(createMenuItem("Sky", "sky.css", scene));

menuBar.getMenus().add(menu);
// stretch menu
menuBar.prefWidthProperty().bind(primaryStage.widthProperty());

// Left and right split pane
SplitPane splitPane = new SplitPane();
splitPane.prefWidthProperty().bind(scene.widthProperty());
splitPane.prefHeightProperty().bind(scene.heightProperty());

// Form on the right
GridPane rightGridPane = new MyForm();

GridPane leftGridPane = new GridPaneControlPanel(rightGridPane);
VBox leftArea = new VBox(10);
leftArea.getChildren().add(leftGridPane);

HBox hbox = new HBox();
hbox.getChildren().add(splitPane);
VBox vbox = new VBox();
vbox.getChildren().add(menuBar);
vbox.getChildren().add(hbox);
root.getChildren().add(vbox);
splitPane.getItems().addAll(leftArea, rightGridPane);

primaryStage.setScene(scene);

primaryStage.show();

}

protected final MenuItem createMenuItem(String label, String css, final Scene scene){
MenuItem menuItem = new MenuItem(label);
ObservableList<String> cssStyle = loadSkin(css);
menuItem.setOnAction(skinForm(cssStyle, scene));
return menuItem;
}

protected final ObservableList<String> loadSkin(String cssFileName) {
ObservableList<String> cssStyle = FXCollections.observableArrayList();
cssStyle.addAll(getClass().getResource(cssFileName).toExternalForm());
return cssStyle;
}

protected final EventHandler<ActionEvent> skinForm
(final ObservableList<String> cssStyle, final Scene scene) {
return (ActionEvent event) -> {
scene.getStylesheets().clear();
scene.getStylesheets().addAll(cssStyle);
};
}

protected final EventHandler<ActionEvent> enableCss(String style, final Scene scene){
return (ActionEvent event) -> {

scene.getStylesheets().clear();
setUserAgentStylesheet(style);
};
}

}

Figure 15-10 depicts the standard JavaFX Modena Look and Feel (theme).

9781430268277_Fig15-10.jpg

Figure 15-10. Modena Look and Feel

Figure 15-11 depicts the Control Style 1 Look and Feel (theme).

9781430268277_Fig15-11.jpg

Figure 15-11. Control Style 1 Look and Feel

How It Works

JavaFX has the capability to apply CSS styles to the scene graph and its nodes just like browsers apply CSS styles to elements in an HTML document object model (DOM). In this recipe, you will be skinning a user interface using JavaFX styling attributes. You basically use the recipe’s UI to apply the various Look and Feels. To showcase the available skins, a menu selection allows the users to choose the Look and Feel to apply to the UI.

Before discussing the CSS styling properties, take a look at how you load the CSS styles to be applied to a JavaFX application. The application in the example uses menu items to allow the user to choose the preferred Look and Feel. When creating a menu item, you’ll create a convenience method to build a menu item that loads the specified CSS and an EventHandler action, via a lambda expression, to apply the chosen CSS style to the current UI. The Modena look and feel is loaded by default. Different look and feels can be applied by passing their respective stylesheets to the setUserAgentStylesheet() method. For instance, to load the Caspian Look and Feel, you simply pass the constant STYLESHEET_CASPIAN to the setUserAgentStylesheet() method. The following code shows how to create these menu items:

MenuItem caspianLnf = new MenuItem("Caspian");
caspianLnf.setOnAction(skinForm(caspian, scene));

Shown next is the code for adding a menu item containing the Sky Look and Feel CSS style, which is ready to be applied to the current UI.

// New Modena Look and Feel
MenuItem modenaLnf = new MenuItem("Modena");
modenaLnf.setOnAction(enableCss(STYLESHEET_MODENA,scene));
menu.getItems().add(modenaLnf);

The setOnAction() method calls a method named enableCss(), which takes a style sheet and the current scene. The code for enableCss() is as follows:

protected final EventHandler<ActionEvent> enableCss(String style, final Scene scene){
return (ActionEvent event) -> {

scene.getStylesheets().clear();
setUserAgentStylesheet(style);
};
}

For each of the other CSS styles, which are not part of the default JavaFX distribution, the menu item creation is a bit different. This is an example of the code that utilizes the convenience method that was previously discussed.

menu.getItems().add(createMenuItem("Control Style 1", "controlStyle1.css", scene));

Calling the createMenuItem() method will also call another convenience method to load the CSS file called loadSkin(). It will also set the menu item’s onAction attribute with an appropriate EventHandler by calling the skinForm() method. To recap, the loadSkin is responsible for loading the CSS file, and the skinForm() method’s job is to apply the skin onto the UI application. Shown here are the convenience methods to build menu items that apply CSS styles to a UI application:

protected final MenuItem createMenuItem(String label, String css, final Scene scene){
MenuItem menuItem = new MenuItem(label);
ObservableList<String> cssStyle = loadSkin(css);
menuItem.setOnAction(skinForm(cssStyle, scene));
return menuItem;
}

protected final ObservableList<String> loadSkin(String cssFileName) {
ObservableList<String> cssStyle = FXCollections.observableArrayList();
cssStyle.addAll(getClass().getResource(cssFileName).toExternalForm());
return cssStyle;
}

protected final EventHandler<ActionEvent> skinForm
(final ObservableList<String> cssStyle, final Scene scene) {
return (ActionEvent event) -> {
scene.getStylesheets().clear();
scene.getStylesheets().addAll(cssStyle);
};
}

Image Note To run this recipe, make sure the CSS files are located in the compiled classes area. Resource files can be loaded easily when placed in the same directory (package) as the compiled class file that is loading them. The CSS files are co-located with this code example file. In NetBeans, you can select Clean and Build Project or you can copy files to your classes’ build area.

Now that you know how to load CSS styles, let’s talk about the JavaFX CSS selectors and styling properties. Like CSS style sheets, there are selectors or style classes associated with Node objects in the scene graph. All scene graph nodes have a method called setStyle() that applies styling properties that could potentially change the node’s background color, border, stroke, and so on. Because all graph nodes extend from the Node class, derived classes will be able to inherit the same styling properties. Knowing the inheritance hierarchy of node types is very important because the type of node will determine the types of styling properties you can affect. For instance, a Rectangle extends from Shape, which extends from Node. The inheritance does not include -fx-border-style, which is the part of node that extends from Region. Based on the type of node, there are limitations to what styles you are able to set. To see a full list of all the style selectors, refer to the JavaFX CSS Reference Guide:

http://docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.html

All JavaFX styling properties are prefixed with -fx-. For example, all Nodes have the styling property to affect opacity, and that attribute is -fx-opacity. Following are selectors that style the JavaFX javafx.scene.control.Labels andjavafx.scene.control.Buttons:

.label {
-fx-text-fill: rgba(17, 145, 213);
-fx-border-color: rgba(255, 255, 255, .80);
-fx-border-radius: 8;
-fx-padding: 6 6 6 6;
-fx-font: bold italic 20pt "LucidaBrightDemiBold";

}
.button{
-fx-text-fill: rgba(17, 145, 213);
-fx-border-color: rgba(255, 255, 255, .80);
-fx-border-radius: 8;
-fx-padding: 6 6 6 6;
-fx-font: bold italic 20pt "LucidaBrightDemiBold";

}

Summary

In this chapter, we covered a variety of topics that deal with JavaFX graphics. We learned how to create images by developing an application that allows one to drag and drop images onto a stage, thereby creating a copy of the image. We then covered recipes; which enable animation of text and also of shapes. Lastly, we learned how to utilize grids and/or CSS to lay out application components.