9.6 Improving the Animator

 < Day Day Up > 



9.6 Improving the Animator

The last program developed in this chapter shows how multiple paradigms can be applied to create more effective solutions. In the animator in Chapter 7, a control panel was included as part of the animator object. This control panel allowed the animator to start, stop, and single step an animation. It also allowed an animation to be sped up or slowed down. This was a useful enhancement, but the control panel was pretty much hacked into the animator. This is obvious because the control panel had to manipulate shared variables internal to the animator to get the control to perform correctly. A good design would decouple the control panel from the animator. The example given here does this. The source code for all parts of this program is contained in Program9.5 (Exhibits 16 through 23).

Exhibit 16: Program9.5a: DrawListener Interface

start example

 import java.util.EventListener; public interface DrawListener extends EventListener {   public void draw(DrawEvent e); } 

end example

9.6.1 Decoupling the Animator from the Control Panel

The first step in this process is to decouple the animator and control panel. When doing this, an intermediate interface, the Controller interface in Exhibit 19 (Program9.5d), is created. The purpose of the controller interface is to allow the animator to work with any generic controller. Thus, the controller given here is only one possible controller for the animator. Another controller could be developed that controls both the animator and the application that is being animated, or a controller could be developed to control multiple animators. Decoupling the controller from the animator raises many more possibilities for how the controller can be implemented.

The controller interface is defined with only a single method, a doRedraw method. This method is called each time a repaint occurs. The controller can only schedule a repaint event if it does not know when that event takes place. The purpose of the doRedraw method is for the animator to notify the controller that the scheduled redraw has taken place. This is the only interaction that the animator needs to have with the controller. Thus, by making this interface with one call, the animator is independent from the controller except when the controller schedules repaint events and the animator informs the controller that the event has taken place.

Other than allowing different controllers to use the animator, another advantage is offered by interfacing the controller and the animator in this manner. In the design presented here, the animator only has to call the controller to let the controller know that it has responded to the paint event. This is significantly less complicated than when the controller was built into the animator and a number of variables were shared. Now the concerns of the different parts of the program are well defined, and problems should be easier to understand and fix, as they should be isolated to smaller parts of the program.

Exhibit 17: Program9.5b: DrawEvent Class

start example

 import java.util.*; import java.awt.*; import java.io.Serializable; public class DrawEvent extends EventObject implements     Serializable {   Graphics myGraphics;   boolean doMove;   /**    * Public constructor that takes all the possible options    * needed for this event    */   public DrawEvent(Object source, Graphics graphics, boolean       doMove) {     super(source);     myGraphics = graphics;     doMove = doMove;   }   /**    * Public constructor that sets the doMove variable to the    * default value of true    */   public DrawEvent(Object source, Graphics graphics) {     this(source, graphics, true);   }   /**    * Accessor method to return the value of the myGraphics    * variable    */   public Graphics getGraphics() {     return myGraphics;   }   /**    * Accessor method to return the value of the doMove    * variable    */   public boolean getDoMove() {     return doMove;   } } 

end example

Exhibit 18: Program9.5c: ControllerAlreadyStartedException

start example

 public class ControllerAlreadyStartedException extends Exception     {   public ControllerAlreadyStartedException() {   }   public ControllerAlreadyStartedException(String s) {     super(s);   } } 

end example

Exhibit 19: Program9.5d: Controller Interface

start example

 public interface Controller {   public boolean doRedraw(); } 

end example

Exhibit 20: Program9.5e: ControllerImp Class

start example

 public class ControllerImpl implements Controller {   // States for this monitor.   private static final int WAITING = 0;   private static final int MOVING = 1;   private static final int DRAWING = 2;   private static final int STOPPED = 3;   private int controllerState = STOPPED;   private int stateAfterDoRedraw = WAITING;   /***************/   private int waitTime = 100; // Time to wait between drawing.   private Animator animator;  // Animator to be used with this                               // controller.   /**    * Public constructor. This just hooks this controller up to    * the animator.    */   public ControllerImpl(Animator animator) {     this.animator = animator;   }   /**    * This method is called by the animator's paint method to    * reset this controller object and to return to the    * animator whether or not the event that caused this repaint    * was from the animator or some other event, such as a    * reSize event. Because it is run from the GUI thread, it    * cannot do a wait in the method.    */   public synchronized boolean doRedraw() {     boolean returnValue;     if (controllerState = = DRAWING)       returnValue = true;     else       returnValue = false;     if (controllerState = = DRAWING) {       controllerState = stateAfterDoRedraw;       notifyAll();     }     return returnValue;   }   /**    * This method is called from the GUI's controller thread    * to do the wait for the amount of time between redraws.    * Because it cannot run in the GUI thread, it can execute a    * wait inside of the method.    */   public synchronized void doWait() {     try {       while (! (controllerState = = WAITING)) {         wait();       }       controllerState = MOVING;       notifyAll();       wait(waitTime); // Post-processing; this hangs the                       // controller thread for waitTime but                       // also allows any notify to get it to                       // start.     } catch(InterruptedException e) {     }   }   /**    * This method is called from the GUI's controller thread    * to signal that the thread is doing a move.    */   public synchronized void doMove() {     try {       while (! ((controllerState = = MOVING) ||           (controllerState = = WAITING))) {         wait();       }       stateAfterDoRedraw = WAITING;       animator.repaint();       if (controllerState = = MOVING) {         controllerState = DRAWING;         notifyAll();       }     } catch(InterruptedException e) {     }   }   /**    * Called from the Controller GUI to stop the animator.    * Because it runs in the thread, wait cannot be called from    * this method. Also, when a button is pressed, the controller    * notifies all waiting threads (in this case, only the GUI    * controller thread).    */   public synchronized void doStop() {     stateAfterDoRedraw = STOPPED;     controllerState = STOPPED;     notifyAll();   }   /**    * Called from the Controller GUI to start the animator.    * Because it runs in the thread, wait cannot be called from    * this method. Also, when a button is pressed, the controller    * notifies all waiting threads (in this case, only the GUI    * controller thread).    */   public synchronized void doStart() {     stateAfterDoRedraw = WAITING;     animator.repaint();     controllerState = DRAWING;     notifyAll();   }   /**    * Called from the Controller GUI to step the animator.    * Because it runs in the thread, wait cannot be called from    * this method. Also, when a button is pressed, the controller    * notifies all waiting threads (in this case, only the GUI    * controller thread).    */   public synchronized void doStep() {     stateAfterDoRedraw = STOPPED;     animator.repaint();     controllerState = DRAWING;     notifyAll();   }   public synchronized void setWaitTime(int waitTime) {     this.waitTime = waitTime;     notifyAll();   } } 

end example

Exhibit 22 (Program9.5g) shows the animator that is implemented with the Controller interface. Note that now the animator consists only of the paint, addDrawListener, and removeDrawListener methods, and that the paint method is significantly simpler than before.

9.6.2 Correcting the Repaint Problem

The doRedraw method also allows a fix for a problem by calling the repaint method. When the repaint method is called from the controller thread, it creates a repaint event in the GUI thread; however, this repaint event is also created when another window passes over the animator or if the animator is resized. In fact, a number of things can cause a repaint event. When the animator from Chapter 7 is run, any of these redraw events could cause the animator to move all the objects in the animation.

To get around this, the doRedraw method returns a Boolean when called. The purpose of this Boolean is to allow the animator to determine whether or not the repaint event being processed originated from the animator. If the repaint event did not originate with the animator, this fact is recorded as part of the DrawEvent, and the application program can then choose to ignore the need to actually move an object as part of the animation. The new DrawEvent is shown in Exhibit 17 (Program9.5b) and now includes a Boolean field doMove that tells the application whether or not the DrawEvent is intended to move the animation a step.

Exhibit 21: Program9.5f: ControlPanel Class

start example

 import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; public class ControlPanel extends JPanel {   private ControllerImpl controllerImpl;   private boolean controllerStarted = false;   /**    * Public constructor. This constructor hooks to the    * ControllerImp for control and then creates the panel with    * all the controls to run the animator.    */   public ControlPanel(ControllerImpl controllerImpl) {     this.controllerImpl = controllerImpl;     createPanel();   }   /**    * This method creates all the controls for the animator and    * hooks all the actions to the controls.    */   private void createPanel() {     setLayout(new FlowLayout());     final JButton startButton = new JButton("Start");     startButton.addActionListener(new ActionListener() {       public void actionPerformed(ActionEvent e) {         JButton jb = (JButton)(e.getSource());         String s = jb.getText();         if (s.equals("Start")) {           controllerImpl.doStart();           startButton.setText("Stop");         }         else {           controllerImpl.doStop();           startButton.setText("Start");         }       }     });     add(startButton);     final JButton stepButton = new JButton("Step");     stepButton.addActionListener(new ActionListener() {       public void actionPerformed(ActionEvent e) {         controllerImpl.doStep();         startButton.setText("Start");       }     });     add(stepButton);     JButton exitButton = new JButton("Exit");     exitButton.addActionListener(new ActionListener() {       public void actionPerformed(ActionEvent e) {         System.exit(0);       }     });     add(exitButton);     add(new Label("Animator Speed"));     final JScrollBar speedControl = new JScrollBar(JScrollBar.         HORIZONTAL, 500, 25, 100, 1000);     speedControl.addAdjustmentListener(new AdjustmentListener()         {       public void adjustmentValueChanged(AdjustmentEvent e) {         controllerImpl.setWaitTime(1000 - e.getValue());       }     });     add(speedControl);   }   /**    * Display the control panel. Note that if by this time the    * control panel is not associated with an animator, it is a    * mistake. Throw control an exception if this happens.    */   public void startControlPanel() throws ControllerAlready       StartedException {     if (controllerStarted) {       throw new ControllerAlreadyStartedException();     }     setVisible(true);     controllerStarted = true;     (new Thread(new ControlThread())).start();   }   /** Thread that runs the animator. This thread sleeps for some    * amount of time specified by sleepTime, then calls repaint,    * which forces a call to the paint method, but in the GUI    * thread. Note that the animatorStopped button allows the    * animator to single step and pause the animation. The    * notify is done in the control frame from the button.    */   private class ControlThread implements Runnable {     public void run() {       while (true) {         controllerImpl.doWait();         controllerImpl.doMove();       }     }   } } 

end example

The decision to delegate to the application the decision of what to do with a DrawEvent not generated from the animator was made for a number of reasons. For example, the application might be written simply to display the state of the object, not to animate it. If that is the policy when a repaint event is generated, the application might always want to be redrawn to show its current state.

Exhibit 22: Program9.5g: Animator Class

start example

 import java.awt.*; import java.awt.image.*; import java.awt.event.*; import java.util.*; import javax.swing.*; public class Animator extends JPanel {   private Vector elementsToDraw = new Vector();   private Controller controller;   /**    * A static method to create an instance of the animator    * using a frame for the animator and a frame for the    * controller.    */   public static Animator createAnimator() {     Animator animator = new Animator();     JFrame animFrame = new JFrame("Generic Animator");     Container animContainer = animFrame.getContentPane();     animContainer.add(animator);     animFrame.setSize(700,450);     animFrame.setLocation(0,100);     animFrame.setVisible(true);     ControllerImpl controllerImpl = new ControllerImpl(animator)         ;     animator.setController(controllerImpl);     ControlPanel controlPanel = new ControlPanel(controllerImpl)         ;     JFrame controlFrame = new JFrame("Control Panel");     Container controlContainer = controlFrame.getContentPanel();     controlContainer.add(controlPanel);     controlFrame.setSize(500,100);     controlFrame.setVisible(true);     try {       controlPanel.startControlPanel();     } catch (ControllerAlreadyStartedException e) {     }     return animator;   }   /**    * Public constructor. If this constructor is used,    * the controller must be set at some point using    * the setController method.    */   public Animator() {   }   /**    * Public constructor. This constructor hooks the animator to    * a controller.    */   public Animator(Controller controller) {     this.controller = controller;   }   /**    * Accessor method that sets the controller for this object.    */   public void setController(Controller controller) {     this.controller = controller;   }   /**    * This method implements the equivalent of the processEvent    * method for the Java Event Model.    */   public void paint(Graphics g) {     DrawEvent de;     super.paint(g);     if ((controller ! = null) && (!controller.doRedraw()))       de = new DrawEvent(this, g, false);     else       de = new DrawEvent(this, g, true);     Vector v;     synchronized(this) {         v = (Vector) elementsToDraw.clone();     }     Enumeration e = v.elements();     while (e.hasMoreElements())       ((DrawListener) e.nextElement()).draw(de);   }   /**    * addListener method for the Java Event Model    */   public synchronized void addDrawListener(DrawListener d) {     elementsToDraw.addElement(d);   }   /**    * removeListener method for the Java Event Model    */   public synchronized void removeDrawListener(DrawListener d) {     elementsToDraw.removeElement(d);   } } 

end example

Another application could be interested in drawing its icons based on the size of the current window, which is contained in the Graphics object. So, if a reSize event on the window generated the repaint call, the application may want to redraw itself at a new size; however, the repaint event might not move the object a step on the path. The animator does not know what the policies of the application are or how the application might implement them, so it simply makes the information on what caused the event available to the application and lets the application decide what it wants to do.

Exhibit 23: Program9.5h: Concurrent Ball Program To Run the New Animator

start example

 import java.awt.*; import java.util.*; /**  * This class animates a ball in a thread using the animator  * component. It is a correct solution.  */ public class ConcurrentBall implements DrawListener, Runnable {   static final int EAST = 0;   static final int WEST = 1;   static final int NORTH = 2;   static final int SOUTH = 3;   int direction;   Path myPath;   Random random;   Animator animator;   /** Constructor. Note that we need to register with the    * animator through the parents class.    */   public ConcurrentBall(Animator animator, int direction) {     this.animator = animator;     this.direction = direction;     random = new Random(direction);   }   /**    * The run method simulates an asynchronous ball. The    * myPath variable is set here and used in the draw method    * and is intended to coordinate the ball thread running in    * this method and the GUI thread (from the animator)    * running in the draw method. This works correctly.    */   public void run() {     animator.addDrawListener(this);       try {         while(true) {           synchronized (this) {             if (direction = = SOUTH) {               myPath = new StraightLinePath(410, 205, 10, 205,                  50);               direction = NORTH;             }             else if (direction = = NORTH) {               myPath = new StraightLinePath(10, 205, 410, 205,                  50);               direction = SOUTH;             }             else if (direction = = EAST) {               myPath = new StraightLinePath(205, 410, 205, 10,                  50);               direction = WEST;             }             else if (direction = = WEST) {             myPath = new StraightLinePath(205, 10, 205, 410,                50);             direction = EAST;           }           wait();         }       Thread.sleep(random.nextInt(10000));       }     } catch (InterruptedException e) {     }   }   /**    * Draw is called each time through the animator loop to draw    * the object. It simply uses the path to calculate the    * position of this object and then draws itself at that    * position. When the end of the path is reached, it notifies    * the ball thread.    */   public synchronized void draw(DrawEvent de) {     Point p = myPath.nextPosition();     Graphics g = de.getGraphics();     g.setColor(Color.red);     g.fillOval((int)p.getX(), (int)p.getY(), 15, 15);     if (! myPath.hasMoreSteps())       notify();   }   /**    * The main method just creates the animator and    * the ball threads and starts them running.    */   public static void main(String args[]) {     Animator animator = Animator.createAnimator();     ConcurrentBall cb1 = new ConcurrentBall(animator, WEST);     (new Thread(cb1)).start();     ConcurrentBall cb2 = new ConcurrentBall(animator, NORTH);     (new Thread(cb2)).start();   } } 

end example

9.6.3 Designing the Controller

A control panel and controller can now be designed that implements the basic functionality of the controller in Chapter 7. The controller is designed using the techniques given in Chapter 3, with some additions because of constraints in using the GUI threads. The first constraint is that the GUI thread is physically one thread, but it will present itself to the controller as two active objects: the animator and the control panel. Even though they are both run in the GUI thread, it is easier to talk about them as different active objects. A third thread, the control thread, generates the timer repaints as shown in Chapter 7. The pseudocode design of the three active objects is given in Exhibit 24.

Exhibit 24: Psuedo-Code Design of the Three Active Objects

start example

Controller Thread

ControlPanel Thread (GUI)

Animator Thread (GUI)

 while(true) {   call doWait   sleep for waitTime   call doMove } 

 while (true) {   select   when (stopped &&     start button)     call doStart   when (running &&     stop button)     call doStop   when (step button)     call doStep } 

 while(true) {   call doMove   createEvent call each listener } 

end example

The pseudo-code implemented here is not implemented in a thread as in Chapter 3, because the control is not actually coming from the thread itself. In the case of the ControlPanel, the active process is the user. In the case of the animator, the active process is generated in response to repaint events, which can come from any number of conditions; however, it is possible to capture these behaviors and abstract them to appear as threads in relation to the passive control component. Also note that a "select" statement with "when" clauses is used in the ControlPanel to show that the control panel acts like an infinite loop that can select one of the three options. The "when" clause then tells when each option can be selected.

Now the passive object can be designed to interact with these active tasks. When designing the passive object (in this case, the controller), it is important to keep in mind that the GUI thread can never execute a wait or do anything else that uses a potentially large or indeterminate amount of time, in the monitor. This places some constraints on the state diagram. A method that is called from the GUI thread must always be able to execute, which means that either the transition that runs in the GUI thread must emanate from every state (such as the doRedraw method) or must never be called from a given state (such as the doStart method, which logically cannot be called from the WAITING or MOVING states). The state diagram for implementing the controller object is given in Exhibit 25. The program to implement this new ControllerImpl object is given in Exhibit 22 (Program9.5g), and the ControlPanel that uses this object is shown in Exhibit 23 (Program9.5h).

Exhibit 25: ControllerImpl Stat Diagram

start example

click to expand

end example

This design raises one last design issue. If the controller thread is waiting and a doStep, doStart, doStop, or setWaitTime method is called, the controller thread should be notified so that it can begin waiting again or respond to the correct state in the monitor. If the waiting time was done with a sleep in the controller thread, there would be no easy way to wake this thread up from one of the calls; therefore, the sleep call has been moved into the monitor and is achieved through the use of a timed wait method call. Now the controller thread can sleep normally but can be notified by a normal notify or notifyAll call in the monitor if the monitor changes state.



 < Day Day Up > 



Creating Components. Object Oriented, Concurrent, and Distributed Computing in Java
The .NET Developers Guide to Directory Services Programming
ISBN: 849314992
EAN: 2147483647
Year: 2003
Pages: 162

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net