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. Write your program as a stand-alone application. Here is an applet version of the program:
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. The main panel class is the container. We need another class to define the drawing surface. This could be 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. 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 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. 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( new ActionListener() { public void actionPerformed(ActionEvent evt) { roll(); } }); add(rollButton, BorderLayout.SOUTH); } // end constructor
Since the main() routine in my stand-alone application will use the pack() method to set the size of the frame, it is essential that dicePanel have a preferred size. If not, the size of the frame will not be set correctly.
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.5.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 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 actionPerformed method stops the timer.
There is also 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. Note that the ActionListener for the timer is defined as an anonymous inner class.
/** * 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(); }
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( new ActionListener() { public void actionPerformed(ActionEvent 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. That class can be written as follows:
import javax.swing.JFrame; /** * A main program that just opens a window that shows a DicePanelWithButton. */ public class DiceWithButton { 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); } }