13.5 Drawing TechniquesHaving learned to walk, let's try a jog. In this section, we'll look at some techniques for doing fast and flicker-free drawing and painting. If you're interested in animation or smooth updating, you should read on.[4]
Drawing operations take time, and time spent drawing leads to delays and imperfect results. Our goal is to minimize the amount of drawing work we do and, as much as possible, to do that work away from the eyes of the user. You'll remember that our TestPattern applet had a blinking problem. It blinked because TestPattern performs several, large, area-filling operations each time its paint() method is called. On a very slow system, you might even be able to see each shape being drawn in succession. TestPattern could be easily fixed by drawing into an off-screen buffer and then copying the completed buffer to the display. To see how to eliminate flicker and blinking problems, we'll look at an applet that needs even more help. TerribleFlicker illustrates some of the problems of updating a display. Like many animations, it has two parts: a constant background and a changing object in the foreground. In this case, the background is a checkerboard pattern and the object is a small, scaled image we can drag around on top of it, as shown in Figure 13.6. Our first version of TerribleFlicker lives up to its name and does a very poor job of updating.
import java.awt.*; import java.awt.event.*; public class TerribleFlicker extends java.applet.Applet implements MouseMotionListener { int grid = 10; int currentX, currentY; Image img; int imgWidth = 60, imgHeight = 60; public void init() { img = getImage( getClass().getResource(getParameter("img")) ); addMouseMotionListener( this ); } public void mouseDragged( MouseEvent e ) { currentX = e.getX(); currentY = e.getY(); repaint(); } public void mouseMoved( MouseEvent e ) { }; // complete MouseMotionListener public void paint( Graphics g ) { int w = getSize().width/grid; int h = getSize().height/grid; boolean black = false; for ( int y = 0; y <= grid; y++ ) for ( int x = 0; x <= grid; x++ ) { g.setColor( (black = !black) ? Color.black : Color.white ); g.fillRect( x * w, y * h, w, h ); } g.drawImage( img, currentX, currentY, imgWidth, imgHeight, this ); } } Try dragging the image; you'll notice both the background and foreground flicker as they are repeatedly redrawn. What is TerribleFlicker doing, and what is it doing wrong? As the mouse is dragged, TerribleFlicker keeps track of its position in two instance variables, currentX and currentY. On each call to mouseDragged(), the coordinates are updated, and repaint() is called to ask that the display be updated. When paint() is called, it looks at some parameters, draws the checkerboard pattern to fill the applet's area, and finally paints a small version of the image at the latest coordinates. Our first, and biggest, problem is that we are updating, but we have neglected to implement the applet's update() method with a good strategy. Because we haven't overridden update(), we are getting the default implementation of the Component update() method, which looks something like this:
// Default implementation of applet update public void update( Graphics g ) { setColor ( backgroundColor ); fillRect( 0, 0, getSize().width, getSize().height ); paint ( g ); } This method simply clears the display to the background color and calls our paint() method. This is almost never the best strategy, but is the only appropriate default for update(), which doesn't know how much of the screen we're really going to paint. Our applet paints its own background, in its entirety, so we can provide a simpler version of update() that doesn't bother to clear the display:
// add to TerribleFlicker public void update( Graphics g ) { paint( g ); } This applet works better because we have eliminated one large, unnecessary, and (in fact) annoying graphics operation. However, although we have eliminated a fillRect() call, we're still doing a lot of wasted drawing. Most of the background stays the same each time it's drawn. You might think of trying to make paint() smarter, so that it wouldn't redraw these areas, but remember that paint() has to be able to draw the entire scene because it might be called in situations when the display isn't intact. The solution is to have update() help out by restricting the area paint() can draw. ClippingThe setClip() method of the Graphics class restricts the drawing area of a graphics context to a smaller region. A graphics context normally has an effective clipping region that limits drawing to the entire display area of the component. We can specify a smaller clipping region with setClip(). How is the drawing area restricted? Well, foremost, drawing operations that fall outside of the clipping region are not displayed. If a drawing operation overlaps the clipping region, we see only the part that's inside. A second effect is that, in a good implementation, the graphics context can recognize drawing operations that fall completely outside the clipping region and ignore them altogether. Eliminating unnecessary operations can save time if we're doing something complex, like filling a bunch of polygons. This doesn't save the time our application spends calling the drawing methods, but the overhead of calling these kinds of drawing methods is usually negligible compared to the time it takes to execute them. (If we were generating an image pixel by pixel, this would not be the case, as the calculations would be the major time sink, not the drawing.) So we can save time in our applet by having our update method set a clipping region that results in only the affected portion of the display being redrawn. We can pick the smallest rectangular area that includes both the old image position and the new image position, as shown in Figure 13.7. This is the only portion of the display that really needs to change; everything else stays the same. An arbitrarily smart update() could save even more time by redrawing only those regions that have changed. However, the simple clipping strategy we've implemented here can be applied to many kinds of drawing, and gives quite good performance, particularly if the area being changed is small. One important thing to note is that, in addition to looking at the new position, our updating operation now has to remember the last position at which the image was drawn. Let's fix our applet so it will use a clipping region. To keep this short and emphasize the changes, we'll take some liberties with design and make our next example a subclass of TerribleFlicker. Let's call it ClippedFlicker:
public class ClippedFlicker extends TerribleFlicker { int nextX, nextY; public void mouseDragged( MouseEvent e ) { nextX = e.getX(); nextY = e.getY(); repaint(); } void clipToAffectedArea( Graphics g, int oldx, int oldy, int newx, int newy, int width, int height) { int x = Math.min( oldx, newx ); int y = Math.min( oldy, newy ); int w = ( Math.max( oldx, newx ) + width ) - x; int h = ( Math.max( oldy, newy ) + height ) - y; g.setClip( x, y, w, h ); } public void update( Graphics g ) { int lastX = currentX, lastY = currentY; currentX = nextX; currentY = nextY; clipToAffectedArea( g, lastX, lastY, currentX, currentY, imgWidth, imgHeight ); paint( g ); } } You should find that ClippedFlicker is significantly faster, though it still flickers. We'll make one more change in the next section to eliminate that. So, what have we changed? First, we've overridden mouseDragged() so that instead of setting the current coordinates of the image, it sets another pair of coordinates called nextX and nextY. These are the coordinates at which we'll display the image the next time we draw it. update() now has the added responsibility of taking the next position and making it the current position, by setting the currentX and currentY variables. This effectively decouples mouseDragged() from our painting routines. We'll discuss why this is advantageous in a bit. update() then uses the current and next coordinates to set a clipping region on the Graphics object before handing it off to paint(). We have created a new, private method to help it do this. clipToAffectedArea() takes as arguments the new and old coordinates and the width and height of the image. It determines the bounding rectangle as shown in Figure 13.6, then calls setClip() to set the clipping region. As a result, when paint() is called, it draws only the affected area of the screen. So, what's the deal with nextX and nextY? By making update() keep track of the next, current, and last coordinates separately, we accomplish two things. First, we always have an accurate view of where the last image was drawn and second, we have decoupled where the next image will be drawn from mouseDragged(). It's important to decouple painting from mouseDragged() because there isn't necessarily a one-to-one correspondence between calls to repaint() and subsequent calls by AWT to our update() method. This isn't a defect; it's a feature that allows AWT to schedule and consolidate painting requests. Our concern is that our paint() method may be called at arbitrary times while the mouse coordinates are changing. This is not necessarily bad. If we are trying to position our object, we probably don't want the display to be redrawn for every intermediate position of the mouse. It would slow down the dragging unnecessarily. If we were concerned about getting every single change in the mouse's position, we would have two options. We could either do some work in the mouseDragged() method itself, or put our events into some kind of queue. We'll see an example of the first solution in our DoodlePad example a bit later. The latter solution would mean circumventing AWT's own event-scheduling capabilities and replacing them with our own, and we don't want to take on that responsibility. Double BufferingNow let's get to the most powerful technique in our toolbox: double buffering. Double buffering is a technique that fixes our flickering problems completely. It's easy to do and gives us almost flawless updates. We'll combine it with our clipping technique for better performance, but in general you can use double buffering with or without clipping. Double buffering our display means drawing into an off-screen buffer and then copying our completed work to the display in a single painting operation, as shown in Figure 13.8. It takes the same amount of time to draw a frame, but double buffering instantaneously updates our display when it's ready. We can get this effect by changing just a few lines of our ClippedFlicker applet. Modify update() to look like the following and add the new offScreenImage instance variable as shown:
... public class DoubleBufferedClipped extends ClippedFlicker { Image offScreenImage; Graphics offScreenGC; public void update( Graphics g ) { if ( offScreenImage == null ) { offScreenImage = createImage( getSize().width, getSize().height ); offScreenGC = img.getGraphics(); } int lastX = currentX, lastY = currentY; currentX = nextX; currentY = nextY; clipToAffectedArea( offScreenGC, lastX, lastY, currentX, currentY, imgWidth, imgHeight ); clipToAffectedArea( g, lastX, lastY, currentX, currentY, imgWidth, imgHeight ); paint( offScreenGC ); g.drawImage(offScreenImage, 0, 0, this); } } ... Now, when you drag the image, you shouldn't see any flickering. The update rate should be about the same as in the previous example (or marginally slower), but the image should move from position to position without noticeable repainting. So, what have we done this time? Well, the new instance variable, offScreenImage, is our off-screen buffer. It is a drawable Image object. We can get an off-screen Image for a component with the createImage() method. createImage() is similar to getImage(), except that it produces an empty image area of the specified size. We can then use the off-screen image like our standard display area by asking it for a graphics context with the Image getGraphics() method. After we've drawn into the off-screen image, we can copy that image back onto the screen with drawImage(). The biggest change to the code is that we now pass paint() the graphics context of our off-screen buffer, rather than that of the on-screen display. paint() is now drawing on offScreenImage; it's our job to copy the image to the display when it's done. This might seem a little suspicious to you, as we are now using paint() in two capacities. AWT calls paint() whenever it's necessary to repaint our entire applet and passes it an on-screen graphics context. When we update ourselves, however, we call paint() to do its work on our off-screen area and then copy that image onto the screen from within update(). Note that we're still clipping. In fact, we're clipping both the on-screen and off-screen buffers. Off-screen clipping has the same benefits we described earlier: AWT should be able to ignore wasted drawing operations. On-screen clipping minimizes the area of the image that gets drawn back to the display. If your display is fast, you might not even notice the savings, but it's an easy optimization, so we'll take advantage of it. We create the off-screen buffer in update() because it's a convenient and safe place to do so. Also, note that our image observer probably won't be called, since drawImage() isn't doing anything nasty like scaling, and the image itself is always available. The dispose() method of the Graphics class allows us to deallocate a graphics context explicitly when we are through with it. This is simply an optimization. If we were creating new graphics contexts frequently (say, in each paint()), we could give the system help in getting rid of them. This might provide some performance improvement when doing heavy drawing. We could allow garbage collection to reclaim the unused objects; however, the garbage collection process might be hampered if we are doing intense calculations or lots of repainting. Off-Screen DrawingIn addition to serving as buffers for double buffering, off-screen images are useful for saving complex, hard-to-produce, background information. We'll look at a simple example: the "doodle pad." DoodlePad is a simple drawing tool that lets us scribble by dragging the mouse, as shown in Figure 13.9. It draws into an off-screen image; its paint() method simply copies the image to the display area.
import java.awt.*; import java.awt.event.*; public class DoodlePad extends java.applet.Applet implements ActionListener { DrawPad dp; public void init() { setLayout( new BorderLayout() ); add( "Center", dp = new DrawPad() ); Panel p = new Panel(); Button clearButton = new Button("Clear"); clearButton.addActionListener( this ); p.add( clearButton ); add( "South", p ); } public void actionPerformed( ActionEvent e ) { dp.clear(); } } class DrawPad extends Canvas { Image drawImg; Graphics drawGr; int xpos, ypos, oxpos, oypos; DrawPad() { setBackground( Color.white ); enableEvents( AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK ); } public void processEvent( AWTEvent e ) { int x = ((MouseEvent)e).getX(), y = ((MouseEvent)e).getY(); if ( e.getID() == MouseEvent.MOUSE_DRAGGED ) { xpos = x; ypos = y; if ( drawGr != null ) drawGr.drawLine( oxpos, oypos, oxpos=xpos, oypos=ypos ); repaint(); } else if ( e.getID() == MouseEvent.MOUSE_PRESSED ) { oxpos = x; oypos = y; } super.processEvent(e); } public void update( Graphics g ) { paint(g); } public void paint( Graphics g ) { if ( drawImg == null ) { drawImg = createImage( getSize().width, getSize().height ); drawGr = drawImg.getGraphics(); } g.drawImage(drawImg, 0, 0, null); } public void clear() { drawGr.clearRect(0, 0, getSize().width, getSize().height); repaint(); } } Give it a try. Draw a nice moose, or a sunset. I just drew a lovely cartoon of Bill Gates. If you make a mistake, hit the Clear button and start over. The parts should be familiar by now. We have made a type of Canvas called DrawPad. The new DrawPad component handles mouse events by enabling both simple mouse events (mouse clicks) and mouse motion events (mouse drags), and then overriding the processEvent() method to handle these events. By doing so, we are simulating the old (Java 1.0) event handling model; in this situation, it's a little more convenient than implementing all the methods of the MouseListener and MouseMotionListener interfaces. The processEvent() method handles MOUSE_DRAGGED movement events by drawing lines into an off-screen image and calling repaint() to update the display. DrawPad's paint() method simply does a drawImage() to copy the off-screen drawing area to the display. In this way, DrawPad saves our sketch information. What is unusual about DrawPad is that it does some drawing outside of paint() or update(). In our clipping example, we talked about decoupling update() and mouseDragged(); we were willing to discard some mouse movements in order to save some updates. In this case, we want to let the user scribble with the mouse, so we should respond to every mouse movement. Therefore, we do our work in processEvent() itself. As a rule, we should be careful about doing heavy work in event handling methods because we don't want to interfere with other tasks the AWT thread is performing. In this case, our line drawing operation should not be a burden, and our primary concern is getting as close a coupling as possible between the mouse movement events and the sketch on the screen. In addition to drawing a line as the user drags the mouse, the part of processEvent() that handles MOUSE_DRAGGED() events maintains a set of old coordinates, to be used as a starting point for the next line segment. The part of processEvent() that handles MOUSE_PRESSED events resets the old coordinates to the current mouse position whenever the user picks up and moves to a new location. Finally, DrawPad provides a clear() method that clears the off-screen buffer and calls repaint() to update the display. The DoodlePad applet ties the clear() method to an appropriately labeled button through its actionPerformed() method. What if we wanted to do something with the image after the user has finished scribbling on it? Well, as we'll see in the next section, we could get the pixel data for the image from its ImageProducer object and work with that. It wouldn't be hard to create a save facility that stores the pixel data and reproduces it later. Think about how you might go about creating a networked "bathroom wall" where people could scribble on your Web pages. |
|