Solution for Programming Exercise 6.4
This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.
Exercise 6.4:
In Exercise 6.3, you wrote a graphical pair-of-dice program where the dice are rolled when the user clicks on the canvas. Now make a pair-of-dice program where the user rolls the dice by clicking a button. The button should appear under the canvas that shows the dice. Also make the following change: When the dice are rolled, instead of just showing the new value, show a short animation during which the values on the dice are changed in every frame. The animation is supposed to make the dice look more like they are actually rolling.
In Exercise 6.3, the entire window was filled by a Canvas. In the new version, we need both a canvas and a button. We need to add both components to the container that acts as the root of the scene graph. I decided to use a BorderPane with the canvas in the center position and the button in the bottom position. (Another option would have been a VBox.) When I first tried this, the button only filled the left half of the bottom position. A BorderPane wants to expand the components that it contains to completely fill their positions in the layout, but as mentioned in Section 6.5, the problem is that a Button has a maximum size that is just large enough to hold the contents of the button. The problem can be fixed by giving the button a bigger maximum width. After I did that, the button filled the entire width of the window. Here is the new start() method:
public void start(Stage stage) { canvas = new Canvas(100,100); draw(); // Draw the original dice. rollButton = new Button("Roll!"); rollButton.setMaxWidth(1000); // so button can grow to full width of window rollButton.setOnAction( e -> roll() ); // When clicked, roll the dice. BorderPane root = new BorderPane(); root.setCenter(canvas); root.setBottom(rollButton); Scene scene = new Scene(root); stage.setScene(scene); stage.setTitle("Dice!"); stage.setResizable(false); stage.show(); } // end start()
The method for drawing the dice was discussed in the solution to Exercise 6.3. But we still have to think about animating the rolling of the dice. The roll() method is responsible for rolling the dice. In the original version, this method simply set the numbers showing on the dice to random values and called draw(). In the new version, we want to repeat this action several times over a period of time. That is, we want an animation in which the action for each frame is to randomize the numbers on the dice and call draw().
Animation is discussed in Subsection 6.3.5. We need to make a subclass of AnimationTimer and provide a definition for the handle() method. That method will be called once in each frame. The timer can be defined using an anonymous nested class (see Subsection 5.8.3.) In my program, the timer is a global variable. It might seem like it would be enough to say:
private AnimationTimer timer = new AnimationTimer() { public void handle( long time ) { die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; draw(); } };
Then, when the roll() method is called, it could simply call time.start(). But there is a big problem with this: The animation should only go on for a short time. In order to stop it, timer.stop() has to be called. But who will call it, and how will they know when to call it? In fact, the timer can stop itself after a certain amount of time has passed. In order to do that, I use a global startTime variable. When roll() starts the timer, it sets the start time to the current time in nanoseconds:
private void roll() { rollButton.setDisable(true); startTime = System.nanoTime(); timer.start(); // start an animation }
Note that it also disables the button, so that the user will not be able to roll the dice while they are already being rolled. Then the timer's handle() method should check the difference between the current time and the start time, and it should stop itself if the time has reached some maximum value. The current time, in nanoseconds, is passed as a parameter to the handle() method. My program stops the animation after one second (1,000,000,000 nanoseconds) has passed. The method must also re-enable the button to make it possible to roll the dice again. So, here is the actual definition of the timer:
private AnimationTimer timer = new AnimationTimer() { // The timer is used to animate "rolling" of the dice. // In each frame, the dice values are randomized. When // the elapsed time reaches 1 second, the timer stops itself. // The rollButton is disabled while an animation is in // progress, so it has to be enabled when the animation stops. public void handle( long time ) { die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; draw(); if ( time - startTime >= 1_000_000_000 ) { timer.stop(); rollButton.setDisable(false); } } };
You might want to slow down the rate at which the numbers on the dice change. Unfortunately, there is no way to slow down the frame rate in an AnimationTimer. However, it is certainly possible to do nothing in some of the frames. To implement that, you could record the time when the dice are drawn, and not draw them again until a certain time has passed. For example, if you want to draw the dice every 1/10 second, The handle() method could be written:
public void handle( long time ) { if ( time - previousDrawTime > 100_000_000 ) { previousDrawTime = time; die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; draw(); if ( time - startTime >= 1_000_000_000 ) { timer.stop(); rollButton.setDisable(false); } } }
Here, previousDrawTime is a global variable. The test "if (time-previousDrawTime > 100_000_000)" will always be true the first time handle() is called, every time the button is clicked. You might think about why that is true.
import javafx.application.Application; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.layout.BorderPane; import javafx.scene.control.Button; import javafx.scene.paint.Color; import javafx.animation.AnimationTimer; /** * Shows a pair of dice that are rolled when the user clicks a button * that appears below the dice. A short animation runs when the * button is clicked, showing the numbers on the dice changing. */ public class RollDiceWithButton extends Application { public static void main(String[] args) { launch(); } //--------------------------------------------------------------------- private int die1 = 4; // The values shown on the dice. private int die2 = 3; private Canvas canvas; // The canvas where the dice are drawn private Button rollButton; // The button that is clicked to roll the dice. private long elapsedTime; // When an animation is running, the number of // nanoseconds for which it has been running. This // is used to end the animation after 1 second. // (One second is 1,000,000,000 nanoseconds. private long startTime; // Time, in nanoseconds, when the animaion started. private AnimationTimer timer = new AnimationTimer() { // The timer is used to animate "rolling" of the dice. // In each frame, the dice values are randomized. When // the elapsed time reaches 1 second, the timer stops itself. // The rollButton is disabled while an animation is in // progress, so it has to be enabled when the animation stops. public void handle( long time ) { die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; draw(); if ( time - startTime >= 1_000_000_000 ) { timer.stop(); rollButton.setDisable(false); } } }; /** * The start() method sets up the GUI, using a BorderPane in which * the canvas is the center component and the button is the bottom * component. An ActionEvent handler is added to the button to * roll the dice when the button is clicked. */ public void start(Stage stage) { canvas = new Canvas(100,100); draw(); // Draw the original dice. rollButton = new Button("Roll!"); rollButton.setMaxWidth(1000); // so button can grow to full width of window rollButton.setOnAction( e -> roll() ); // When clicked, roll the dice. BorderPane root = new BorderPane(); root.setCenter(canvas); root.setBottom(rollButton); Scene scene = new Scene(root); stage.setScene(scene); stage.setTitle("Dice!"); stage.setResizable(false); stage.show(); } // end start() /** * Roll the dice by starting an animation that randomizes * the values on the dice in each frame. The animation will * last for one second, and the rollButton is disabled while * the animation is in progress. This method is called * when the user clicks the roll button. */ private void roll() { rollButton.setDisable(true); startTime = System.nanoTime(); timer.start(); // start an animation } /** * Draw a die with upper left corner at (x,y). The die is * 35 by 35 pixels in size. The val parameter gives the * value showing on the die (that is, the number of dots). */ private void drawDie(GraphicsContext g, int val, int x, int y) { g.setFill(Color.WHITE); g.fillRect(x, y, 35, 35); g.setStroke(Color.BLACK); g.strokeRect(x+0.5, y+0.5, 34, 34); g.setFill(Color.BLACK); if (val > 1) // upper left dot g.fillOval(x+3, y+3, 9, 9); if (val > 3) // upper right dot g.fillOval(x+23, y+3, 9, 9); if (val == 6) // middle left dot g.fillOval(x+3, y+13, 9, 9); if (val % 2 == 1) // middle dot (for odd-numbered val's) g.fillOval(x+13, y+13, 9, 9); if (val == 6) // middle right dot g.fillOval(x+23, y+13, 9, 9); if (val > 3) // bottom left dot g.fillOval(x+3, y+23, 9, 9); if (val > 1) // bottom right dot g.fillOval(x+23, y+23, 9,9); } /** * The draw() method just draws the two dice and draws * a two-pixel wide blue border around the canvas. */ private void draw() { GraphicsContext g = canvas.getGraphicsContext2D(); g.setFill(Color.rgb(200,200,255)); g.fillRect(0,0,100,100); g.setStroke( Color.BLUE ); g.strokeRect(1,1,98,98); drawDie(g, die1, 10, 10); drawDie(g, die2, 55, 55); } } // end class RollDiceWithButton