[ Exercises | Chapter Index | Main Index ]

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.


Discussion

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 number of frames have passed. In order to do that, I use a global frameNumber variable. When roll() starts the timer, it sets the frame number to 0:

private void roll() {
   frameNumber = 0;
   rollButton.setDisable(true);
   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 increment the frame number, and it should stop itself if the frame number has reached some maximum value. It 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 number of frames reaches 60, 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();
        frameNumber++;
        if (frameNumber == 60) {
            timer.stop();
            rollButton.setDisable(false);
        }
    }
};

The dice will change for 60 frames or about one second (if JavaFX animation is working correctly). It might be that changing the dice every 1/60 second is too fast. 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. For example, if you only want to change the dice every 10 frames, you can begin the handle() method by saying

if (frameNumber % 10 != 0) {
       // do nothing if frameNumber is not a multiple of 10
    return;
}

The Solution

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.
 */
public class RollDiceWithButton extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    //---------------------------------------------------------------------
    
   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 int frameNumber;  // When an animation is running, the number of
                             //    frames for which it has been running.  This
                             //    is used to end the animation after 60 frames.

   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 number of frames reaches 60, 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();
           frameNumber++;
           if (frameNumber == 60) {
               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 60 frames, 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() {
       frameNumber = 0;
       rollButton.setDisable(true);
       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

[ Exercises | Chapter Index | Main Index ]