Graphical User Interface Programming for Multi-Platform Applications in Java 1.1.
or
GUI Programming in Java for Everyone
Part 4 Rubberbanding and Animation
(Version 1)

Joseph Bergin, Pace University

Introduction

In the previous part we learned some things about drawing. The results were less than satisfying, since any sort of animation using those techniques will leave us with very choppy motion. Here we will learn how to correct this by using off screen bitmaps and double buffering.We will also see a better way to keep our graphical figures connected with lines as they move.

Last Updated: September 26, 2001


Concepts

In order to animate a drawing in a panel or other component, the original contents need to be erased and the new contens drawn in their place. This can result in a blinking (flickering) display and very choppy effects. In order to handle this problem, the usual solution is to draw the new figure off screen while leaving the original visible and then suddenly showing the new figure. This is called double buffering and uses an off screen bitmap.

The applet that we'll use to illustrate also has geometric figures like rectangles, connected by linking lines. This time, however, we will allow the user to select any one of the figures and then drag it to a new position. All of the connections (and labels) will then be restored. We will also show a smooth animation of a simle drawing and show how to animate a more complex one.

Since we are keeping this simple, there are four operations. The first is select. A figure is selected by clicking the mouse on it. The selected figure will be shown in a special way by putting "handles" at its corners. The user can then use the mouse to drag it and an outline of the figure will track with the mouse. When the user releases the mouse button at the new location, the lines are restored. Finally a button will start a simple animation.

The files we will discuss here can be found in the files directory.

Figures

The figures we will utilize here are a bit more sophisticated than those we saw in part 3. We will want to be able to tell say if the mouse is currently in a given figure. We shall also represent the connections themselves as objects here (next section) so that we can more easily label them and move them. We also want to know if a given object is selected. Our figures now form a hierarchy with Figure at the root and several sub classes such as RectangleFigure and Oval Figure. The Connection class will also extend Figure.

public abstract class Figure implements java.io.Serializable
{	private Rectangle bounds;
	private String label;
	private boolean selected = false;
	
	public Figure(String label, int x, int y, int w, int h)
	{	bounds = new Rectangle(x, y, w, h);
		this.label = label;
	}
	
	public void draw(Graphics g)
	{	drawShape(g);
		Color c = g.getColor();
		g.setColor(Color.white);
		fillShape(g);
		g.setColor(Color.red);
		drawLabel(g);
		g.setColor(c);
		if(selected) drawSelectors(g);
	}
	
	public void drawOutline(Graphics g)
	{	drawShape(g);
	}

	public abstract void drawShape(Graphics g);
	public abstract void fillShape(Graphics g);

	public void drawLabel(Graphics g)
	{	g.drawString(label, bounds.x + bounds.width/2, bounds.y + bounds.height/2);
	}

	
	public Connection connect(String label, Figure b)
	{	return new Connection(label, this, b);
	}
	
	public boolean isSelected(){ return selected;}
	public void setSelected(boolean t){selected = t;}
	public void toggleSelected(){selected = !selected;}
	
	public void drawSelectors(Graphics g)
	{	g.fillRect(bounds.x - 2 , bounds.y - 2, 4, 4);
		g.fillRect(bounds.x + bounds.width - 2 , bounds.y-2, 4, 4);
		g.fillRect(bounds.x-2 , bounds.y + bounds.height - 2, 4, 4);
		g.fillRect(bounds.x + bounds.width - 2 , bounds.y + bounds.height - 2, 4, 4);
	}
	
	public Rectangle rectangle(){ return bounds; }
	public Point location(){ return new Point(bounds.x, bounds.y); }
	public Point center(){ return new Point(bounds.x+bounds.width/2, bounds.y+bounds.height/2); }
	public Dimension dimension(){ return new Dimension(bounds.width, bounds.height);}
	
	public void move(int x, int y)
	{	bounds.x = x; bounds.y = y;
	}
	
	public void move(Point p)
	{	bounds.x = p.x; bounds.y = p.y;
	}
	
	public boolean contains(Point p)
	{	return bounds.contains(p);
	}
	
	public boolean contains(int x, int y)
	{	return bounds.contains(x, y);
	}
	
	protected String label()
	{	return label;
	}
}


This class is abstract, so it can't be instantiated. It has two abstract methods, drawShape and fillShape. These are implemented only in subclasses. The draw and drawOutline method implemented here use these abstract methods. In effect, these two methods represent a Strategy for drawing. The subclasses will provide details to this strategy in the drawShape and fillShape methods appropriate for each subclass. An oval will "drawShape" differently from a rectangle, but the strategy for drawiang both is captured in the superclass.

Figures know their location and size. Any such figure knows if it is selected, and we have three methods for dealing with selection. We also can ask a figure if it contains a coordinate given either as a point or as an x, y pair. Additionally, a figure can draw its own selectors. These are small (4 by 4 pixel) rectangles at the corners of its bounding rectangle.

Note that to connect a Figure to another, we actually tell one of the Figures to connect to the other, giving a label for the resulting Connection. This connect method acts like a Factory Method for Connections. In other words it behaves as if it were a constructor for the Connection class.

An example of a Figure is an OvalFigure, which extends the above class. We implement the draw method to draw a filled in oval and repaint the label of the figure. DrawOutline, on the other hand, just draws the outline. The label isn't redrawn. We use drawOutline while we are dragging a figure. It draws faster and it gives us some feedback as we drag. The other Figure subclasses are very similar -- except the Connection class which we take up next.

public class OvalFigure extends Figure implements java.io.Serializable
{	public OvalFigure(String label, int x, int y, int w, int h)
	{	super(label, x, y, w, h);
	}
	
	public void drawShape(Graphics g)
	{	Rectangle bounds = rectangle();
		g.drawOval(bounds.x, bounds.y, bounds.width, bounds.height);
	}
	
	public void fillShape(Graphics g)
	{	Rectangle bounds = rectangle();
		g.fillOval(bounds.x+1, bounds.y+1, bounds.width - 2, bounds.height - 2);
	}	
}

Notice that most of the work of this class is actually done in the Figure class, rather than here. Other figures just draw and fill differently depending on the shape represented by the class.

Connections

Connections are Figures that connect other figures--lines, in other words. We won't select them or try to drag them.

public class Connection extends Figure implements java.io.Serializable
{	private Figure first;
	private Figure second;

	public Connection(String label, Figure first, Figure second)
	{	super(label, 0,  0, 0, 0);
		this.first = first;
		this.second = second;
	}

	public void drawShape(Graphics g)
	{	int x1 = first.center().x, y1 = first.center().y, x2 = second.center().x, y2 = second.center().y;
		g.drawLine(x1, y1, x2, y2);	
	}
	
	public void fillShape(Graphics g)
	{	// nothing
	}
	
	public void drawLabel(Graphics g)
	{	int x1 = first.center().x, y1 = first.center().y, x2 = second.center().x, y2 = second.center().y;
		g.drawString(label(), (x1 + x2)/2+2, (y1 + y2)/2-2);
	}
}

Connections remember the two figures that they connect and when told to draw, draw a line between the centers of the figures. The label is put at the center of this line also. There is nothing to do to "fill" a line. Note that if we move one of the figures that is connected to another, then the one moved will have a new location. When the Connection is later drawn the new location of the figure at the end will be used to draw the line. Therefore, the figures will stay connected by the connections as they move. This is rubberbanding. It is if the figures are connected by rubber bands. Of course we can draw the figures without drawing the Connections, in which case the lines will remain in their old locations.

Animation and Double Buffering

In keeping with our simplicity, the Applet we show here isn't very interesting. In particular, it has a fixed set of figures. An extension would add an option of drawing additional figures with the mouse, perhaps by providing a tool palette with an icon for each kind of figure. When an icon was selected, that kind of figure could be drawn with the mouse, perhaps by clicking in the north-west corner and dragging down to the south-east before releasing the mouse. The object would then be created and drawn. Here, however we just create and draw a few figures when we create the Applet. Here we create four Figures of various kinds and five Connections. They are created at their declaration points after init. Each is inserted into an ArrayList in init. We use separate ArrayLists for the Figures and the Connections, since we want to draw all the Connections first and the Figures later. This will let us draw the Connections from center to center of the Figures and the figure will then cover the part that is "inside" the Figure. See the doDraw method below to see this. It draws all of the Figures and Connections in our Applet.

public class PaintDrawApplet extends Applet
{	private TestCanvas drawArea = new TestCanvas();
	private Figure selectedFigure = null;
	private boolean dragging = false;
	
	public void init() 
	{	add (drawArea);
		add(moveButton);
		moveButton.addActionListener(new MoveListener());
		figures.add(a);
		figures.add(b);
		figures.add(c);
		figures.add(d);
		connections.add(ab);
		connections.add(ca);
		connections.add(bc);
		connections.add(ad);
		connections.add(bd);
	}
	
	private RoundRectangleFigure a = new RoundRectangleFigure("A", 30, 50, 50, 50);
	private OvalFigure b = new OvalFigure("B", 200, 50, 50, 50);
	private RectangleFigure c = new RectangleFigure("C", 30, 150, 50, 50);
	private OvalFigure d = new OvalFigure("D", 100, 100, 60, 30);

	private Connection ab = a.connect("AB", b);
	private Connection ca = c.connect("CA", a);
	private Connection bc = b.connect("BC", c);
	private Connection ad = a.connect("AD", d);
	private Connection bd = b.connect("BD", d);

	private ArrayList figures = new ArrayList();
	private ArrayList connections = new ArrayList();
	
	private Button moveButton = new Button("Animate");
	
	public Dimension getPreferredSize()
	{	return new Dimension(600,400);
	}
	
	public Dimension getMinimumSize()
	{	return new Dimension(500, 300);
	}
	
	class TestCanvas extends Canvas
	{	private Image offscreen = null;
		private static final boolean doubleBuffering = false;
		private Graphics og = null; // The offscreen graphics context. 
		
		public TestCanvas()
		{	addMouseListener(new HitListener());
			addMouseMotionListener(new DragListener());	
			setBackground(Color.white);
		}
		
		public void invalidate()
		{	super.invalidate();
			offscreen = null;
		}
		
		public Dimension getPreferredSize()
		{	Dimension d = PaintDrawApplet.this.getPreferredSize();
			return new Dimension(d.width - 100, d.height);
		}
 
		public Dimension getMinimumSize()
		{	return new Dimension(400, 300);
		}
		
		public void update(Graphics g)
		{	if(doubleBuffering) 
			{	paint(g); 
			}
			else 
			{	super.update(g);
			}
		}
		
		public void flip(Figure f)
		{	Graphics g = getGraphics();
			g.setXORMode(getBackground());
			f.drawOutline(g); // Try draw here instead of drawOutline for a different effect
			g.dispose();
		}

		private void doDraw(Graphics g)
		{	Iterator e = connections.iterator();
			while(e.hasNext())
				((Figure) e.next()).draw( g);
			e = figures.iterator();
			while(e.hasNext())
				((Figure) e.next()).draw( g);
		}
 
		public void paint( Graphics g ) 
		{	if(!doubleBuffering)
			{ 	doDraw(g);
				return;
			}
			Dimension d = getSize(); int h = d.height; int w = d.width;
			if(offscreen == null) offscreen = createImage(w, h);
			Graphics og = offscreen.getGraphics();
			og.setClip(0, 0, w, h);
			doDraw(og); // equivalent to the next few statements (marginally faster)
						
			g.drawImage(offscreen, 0, 0, null);
	    	og.setColor(getBackground()); // Clear the image for next paint.
	   		og.fillRect(0, 0, w, h);
			og.dispose();
		}
	}
	
	class MoveListener implements ActionListener
	{	public void actionPerformed(ActionEvent e)
		{	for (int i = 0; i < 120; ++i)
			{	drawArea.flip(b);
				b.move(b.location().x, b.location().y + 2);
				drawArea.flip(b);
				pause(3);
			}
			for (int i = 0; i < 120; ++i)
			{	drawArea.flip(b);
				b.move(b.location().x + 2, b.location().y);
				drawArea.flip(b);
				pause(3);
			}
			for (int i = 0; i < 120; ++i)
			{	drawArea.flip(b);
				b.move(b.location().x, b.location().y - 2);
				drawArea.flip(b);
				pause(3);
			}
			for (int i = 0; i < 100; ++i)
			{	drawArea.flip(b);
				b.move(b.location().x - 2, b.location().y);
				drawArea.flip(b);
				pause(3);
			}
			drawArea.repaint();
		}
		
		private void pause(int n)
		{	try{Thread.currentThread().sleep(n);}catch(InterruptedException e){}
		}
	}

	class HitListener extends MouseAdapter
	{	public void mouseClicked(MouseEvent e)
		{	Point p = e.getPoint();
			Iterator v = figures.iterator();
			selectedFigure = null;
			boolean doDraw = false;
			while(v.hasNext())
			{	Figure f = (Figure)v.next();
				if(f.contains(p))
				{ 	if(! f.isSelected())
					{ 	doDraw = true;
						f.setSelected(true);
						selectedFigure = f;
					}
				}
				else
				{	if(f.isSelected())
					{ 	doDraw = true;
						f.setSelected(false);
					}
				}
			}
			if (doDraw) drawArea.repaint();
		}
		
		public void mousePressed(MouseEvent e)
		{	if(selectedFigure != null && selectedFigure.contains(e.getPoint()))
				dragging = true;
		}
		
		public void mouseReleased(MouseEvent e)
		{	dragging = false;
			if(selectedFigure != null)
			{	selectedFigure.setSelected(false);
				selectedFigure = null;
			}
			drawArea.repaint();
		}
	}
	
	class DragListener extends MouseMotionAdapter
	{	public void mouseDragged(MouseEvent e)
		{	if(dragging && selectedFigure != null)
			{	drawArea.flip(selectedFigure);
				selectedFigure.move(e.getPoint());
				drawArea.flip(selectedFigure);
			}
		}
	}
}

The actual drawing is done in a TestCanvas object. TestCanvas is a subclass of Canvas in which we override a few methods. We will also add mouse listeners to this canvas but will discuss this only below. The animation is handled by an ActionListener attached to a Button. The animation will take one of the Figures, the one pointed to by variable b, and move it through most of a square, not quite completing the square. The pause method is used to slow down the animation so it doesn't move faster than the eye can follow.

In order to animate a figure, we first need to erase the old figure, move the figure and then redraw it. There are a variety of ways to do this, but one of the best is to "flip" the figure. If we set the draw mode of a graphics object to XORMode, and then draw a figure for a second time, it will disappear. We do this before we move the object. If we then move it and flip it again, it will seem to reappear in a new location. This is all we need to do for simple drawings, though for more complex things like Images it will give us a very unsatisfactory result. The image will seem to flicker like an old time movie. The solution to this is called double buffering. We don't need it here and this application is too simple even to demonstrate it, though we show the technique for solving the problem here as well if it occurs in your work.

Recall that you build a paint method for drawing in Java, but you actually call the repaint method, rather than the paint method. The repaint method sees to clearing the old figure before it draws the new one by calling your paint method. It actually does this by calling update. Normally you don't override either repaint or update, but to do double buffering you will want to override update.

In this Applet we try to show two different techniques, ordinary drawing and double buffering. Depending on the value of the constant doubleBuffering we can execute either one. In this case the results are the same so the simplest (flipping) is best. But in a complex drawing or image in which flickering is a problem, you can use the other.

If we are not doing double buffering, then things are very simple. We don't need to override the update method, and we just repaint the contents of the canvas as needed. Here our paint method just calls doDraw, which first draws all of the Connections and then draws all of the Figures. Nothing more is needed.

On the other hand, if double buffering is needed to avoid flicker, you first want to override update so that it just calls paint without clearing the background first. The paint method then becomes a bit more complex. Instead of drawing directly in the canvas, we are going to draw in a new Image object. This is our offscreen bitmap. We then paint paint this image into our canvas.

 			Dimension d = getSize(); int h = d.height; int w = d.width;
			if(offscreen == null) offscreen = createImage(w, h);
			Graphics og = offscreen.getGraphics();
			og.setClip(0, 0, w, h);
			doDraw(og); // equivalent to the next few statements (marginally faster)
						
			g.drawImage(offscreen, 0, 0, null);
	    	og.setColor(getBackground()); // Clear the image for next paint.
	   		og.fillRect(0, 0, w, h);
			og.dispose();


We create the image of the appropriate size and then get its Gaphics object. We draw with this new Gaphics object which draws into the new Image. Finally we draw the Image in the usual Graphics context. Here we do a bit more, by clearing the offscreen image at this time, which has no effect on the visible image, but which gets us ready to draw in it again.

Selection and Dragging

We now want to be able to select and drag the Figures in our drawing. First, each Figure knows if it selected and has methods to retrieve and set this information. If a Figure is selected it will draw its "handles" when we draw it. These handles are just little rectangles at the corners of its bounding rectangle. If we were allowing reshaping of the Figures, we would permit "grabbing" these handles with the mouse to initiate the reshaping. We don't do that here, so the handles only give us visual feedback on which Figure is selected. We select a Figure by clicking in its bounding rectangle. The mouseClicked method of the MouseAdapter does this. Whenever the mouse is clicked we compare the click point with the bounding rectangles of each of our Figures. Note that we can also unselect a figure if it is selected by clicking in it.

Once a Figure is selected, we can drag it. To do this requires two methods of different classes. First the mousePressed method of the MouseAdapter takes the original press in the figure and sets up required information to handle the drag. Then the mouseMoved method of a new MouseMotionAdapter is called repeatedly to see if the mouse has been moved. If it has we flip the selected figure, move it to the mouse position, and then flip it again.