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 pair-of-dice panel where the dice are rolled when the user clicks on the panel. Now make a pair-of-dice program in which the user rolls the dice by clicking a button. The button should appear under the panel 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, there was a single panel, which was being used as a drawing surface. In the new version, there are two panels: One is the drawing surface on which the dice are drawn; the other is a container that holds the button and the drawing surface panel. If we write a class to define the container, we need another class to define the drawing surface. One way to write that class is as a simple nested class that contains only a paintComponent method. Since it is so simple, I decided to define it as an anonymous inner class:
JPanel dicePanel = new JPanel() { public void paintComponent(Graphics g) { super.paintComponent(g); // fill with background color. Graphics2D g2 = (Graphics2D)g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); drawDie(g, die1, 10, 10); // Just draw the dice. drawDie(g, die2, 55, 55); } };
This just makes dicePanel into a JPanel object that contains a paintComponent method that differs from the one defined in the JPanel class.
The constructor of the main class is responsible for setting up the user interface and the event handling. It sets the layout manager of the panel to be a BorderLayout, creates the drawing surface and button, adds the drawing surface in the CENTER position and the button in the SOUTH position. It adds a blue border to the panel, and leaves a gap in the border layout through which a blue background color will show. It also adds an action listener to the button that will call the roll() method to roll the dice when the button is pressed. Here is the complete constructor:
public DicePanelWithButton() { setLayout(new BorderLayout(2,2)); setBackground(Color.BLUE); // Will show through the gap in the BorderLayout. setBorder(BorderFactory.createLineBorder(Color.BLUE,2)); JPanel dicePanel = new JPanel() { // the drawing surface, where dice are shown public void paintComponent(Graphics g) { super.paintComponent(g); // fill with background color. Graphics2D g2 = (Graphics2D)g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); drawDie(g, die1, 10, 10); // Just draw the dice. drawDie(g, die2, 55, 55); } }; dicePanel.setPreferredSize( new Dimension(100,100) ); dicePanel.setBackground( new Color(200,200,255) ); // light blue add(dicePanel, BorderLayout.CENTER); JButton rollButton = new JButton("Roll!"); // the button that rolls the dice rollButton.addActionListener( evt -> roll() ); add(rollButton, BorderLayout.SOUTH); } // end constructor
Here, the ActionListener for the "Roll" button is specified as a lambda expression (Section 4.5), using the fact that ActionListener is a functional interface. The effect is that when the user clicks the button, the roll() method will be called.
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 repaint(). 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 repaint(). The code for doing one frame goes in the ActionListener that responds to events from the timer. The roll() method simply creates the timer and starts it running. (Timers and animation are discussed in Subsection 6.4.1.)
There is, however, one big problem: How is the timer stopped? If it's not stopped, the dice will keep rolling forever! The solution is not so hard. We have to stop the timer after a certain number of frames. The action listener for the timer can keep track of how many frames it has handled, and after a certain number of frames it can stop the timer. After some experimentation, I found that 10 frames, with a delay between frames of 100 milliseconds, looks pretty good. So in the tenth frame, the action listener stops the timer.
There is still one little problem: The user might click the "Roll" button while the animation is in progress. If the dice are already ready rolling, it doesn't make sense to start another animation. So, the program needs a way of determining whether an animation is in progress when the "Roll" button is clicked; if it is, then the click should be ignored. In my program, there is a Timer variable that is set to a non-null value when an animation is in progress. When the timer is stopped, the animation is finished, and the Timer variable is set back to null. The roll() method checks the Timer variable to determine whether or not an animation is already in progress. (Another way of handling the problem would be to disable the Roll button while the animation is in progress.) Here's the roll() method.
/** * Run an animation that randomly changes the values shown on * the dice 10 times, every 100 milliseconds. */ private void roll() { if (timer != null) return; timer = new Timer(100, new ActionListener() { int frames = 1; public void actionPerformed(ActionEvent evt) { die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; repaint(); frames++; if (frames == 10) { timer.stop(); timer = null; } } }); timer.start(); }
Note that in this case, the ActionListener is defined as an anonymous inner class. It would not be possible to substitute a lambda expression here, because the anonymous class has an instance variable, frames, and there is no way to add an instance variable to a lammbda expression. A lambda expression would be possible if frames were made into a global variable in the containing class, but frames is not a natural part of that class. And frames cannot be a local variable in the roll() method, because it is not effectively final, and local variables can only be used in lambda expressions and local classes if they are final or effectively final. (See Subsection 5.8.4.)
For this program, I decided to put the main() routine for the program in a separate class, just to remind you that it can be done that way!
import java.awt.*; import java.awt.event.*; import javax.swing.*; /** * Shows a pair of dice that are rolled when the user clicks a button * that appears below the dice. */ public class DicePanelWithButton extends JPanel { private int die1 = 4; // The values shown on the dice. private int die2 = 3; private Timer timer; // Used to animate rolling of the dice. /** * The constructor sets up the panel. It creates the button and * the drawing surface panel on which the dice are drawn and puts * them into a BorderLayout. It adds an ActionListener to the button * that rolls the dice when the user clicks the button. */ public DicePanelWithButton() { setLayout(new BorderLayout(2,2)); setBackground(Color.BLUE); // Will show through the gap in the BorderLayout. setBorder(BorderFactory.createLineBorder(Color.BLUE,2)); JPanel dicePanel = new JPanel() { public void paintComponent(Graphics g) { super.paintComponent(g); // fill with background color. drawDie(g, die1, 10, 10); // Just draw the dice. drawDie(g, die2, 55, 55); } }; dicePanel.setPreferredSize( new Dimension(100,100) ); dicePanel.setBackground( new Color(200,200,255) ); // light blue add(dicePanel, BorderLayout.CENTER); JButton rollButton = new JButton("Roll!"); rollButton.addActionListener( evt -> roll() ); add(rollButton, BorderLayout.SOUTH); } // end constructor /** * 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). */ void drawDie(Graphics g, int val, int x, int y) { g.setColor(Color.white); g.fillRect(x, y, 35, 35); g.setColor(Color.black); g.drawRect(x, y, 34, 34); 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); } /** * Run an animation that randomly changes the values shown on * the dice 10 times, every 100 milliseconds. */ private void roll() { if (timer != null) return; timer = new Timer(100, new ActionListener() { int frames = 1; public void actionPerformed(ActionEvent evt) { die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; repaint(); frames++; if (frames == 10) { timer.stop(); timer = null; } } }); timer.start(); } } // end class DicePanelWithButton
To use this in a stand-alone application, we need a class that defines the main() routine of the program. For this exercise, I used a separate class, which can be written as follows:
import javax.swing.JFrame; /** * A main program that just opens a window that shows a DicePanelWithButton. */ public class DiceWithButtonMain { public static void main(String[] args) { JFrame window = new JFrame(); DicePanelWithButton content = new DicePanelWithButton(); window.setContentPane(content); window.pack(); window.setLocation(100,100); window.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); window.setResizable(false); // User can't change the window's size. window.setVisible(true); } }