The first thing I ever saw Java do was an animation: a large red Hi there! that ran across the screen from the right to left. Even that simple form of animation was enough to make me stop and think, "this is really cool."
That sort of simple animation takes only a few methods to implement in Java, but those few methods are the basis for any Java applet that you want to update the screen dynamically-for something as simple as flashy animation applets, or for more complex applets that may need to be updated based on data they get from the user, from databases connected to over the network, or from any other source.
Animation in Java is accomplished through various interrelated parts of the Java Abstract Windowing Toolkit (awt). Today you'll learn the fundamentals of animation in Java: how the various parts of the system all work together so that you can create moving figures and dynamically updatable applets. Specifically, you'll explore the following:
Throughout today, you'll also work with lots of examples of real applets that create animation or perform some kind of dynamic movement.
Animation in Java involves two basic steps: constructing a frame of animation, and then asking Java to paint that frame. You repeat these steps as necessary to create the illusion of movement. The basic, static graphical applets that you created yestersection taught you how to accomplish the first part; all that's left is how to tell Java to paint a frame.
The paint() method, as you learned yestersection, is called whenever an applet needs to be painted-when the applet is initially drawn, when the window containing it is moved, or when another window is moved from over it. You can also, however, ask Java to repaint the applet at a time you choose. So, to change the appearance of what is on the screen, you construct the image or "frame" you want to paint, and then ask Java to paint this frame. If you do this repeatedly, and fast enough, you get animation inside your Java applet. That's all there is to it.
Where does all this take place? Not in the paint() method itself. All paint() does is put dots on the screen. paint(), in other words, is responsible only for the current frame of the animation. The real work of changing what paint() does, of modifying the frame for an animation, actually occurs somewhere else in the definition of your applet.
In that "somewhere else," you construct the frame (set
variables for paint() to
use, create Color or Font
or other objects that paint()
will need), and then call the repaint()
method. repaint() is the
trigger that causes Java to call paint()
and causes your frame to get drawn.
Technical Note |
Because a Java applet can contain many different components that all need to be painted (as you'll learn later this week), and in fact, applets can be embedded inside a larger Java application that also paints to the screen in similar ways, when you call repaint() (and therefore paint()) you're not actually immediately drawing to the screen as you do in other window or graphics toolkits. Instead, repaint() is a request for Java to repaint your applet as soon as it can. Also, if too many repaint() requests are made in a short amount of time, the system may only call repaint() once for all of them. Much of the time, the delay between the call and the actual repaint is negligible. However, for very tight loops, the awt may collapse several calls to repaint() into one. Keep this in mind as you create your own animation. |
Remember start() and stop() from Section 8, "Java Applet Basics"? These are the methods that trigger your applet to start and stop running. You didn't use start() and stop() yestersection because the applets on that section did nothing except paint once. With animation and other Java applets that are actually processing and running over time, you'll need to make use of start() and stop() to trigger the start of your applet's execution, and to stop it from running when you leave the page that contains that applet. For many applets, you'll want to override start() and stop() for just this reason.
The start() method triggers the execution of the applet. You can either do all the applet's work inside that method, or you can call other object's methods in order to do so. Usually, start() is used to create and begin execution of a thread so the applet can run in its own time.
stop(), on the other hand, suspends an applet's execution so when you move off the page on which the applet is displaying, it doesn't keep running and using up system resources. Most of the time when you create a start() method, you should also create a corresponding stop().
There's one more part to the animation mix that you'll have to know about, and that's threads. I'm going to discuss threads in a lot greater detail later on in this lesson (and in even more detail on Section 18, "Multithreading") but for now here's the basic idea: Anything you do in a Java program that runs continually and takes up a lot of processing time should run in its own thread. Animation is one of these things. To accomplish animation in Java, therefore, you use the start() method to start a thread, and then do all your animation processing inside the thread's run() method. This allows the animation to run on its own without interfering with any other parts of the program.
Explaining how to do Java animation is more of a task than actually showing you how it works in code. An example will help make the relationship between all these methods clearer.
Listing 10.1 shows a sample applet that uses basic applet animation techniques to display the date and time and constantly updates it every second, creating a very simple animated digital clock (a frame from that clock is shown in Figure 10.1).
Figure 10.1 : The digital clock.
This applet uses the paint(), repaint(), start(), and stop() methods. It also uses threads. For this discussion, we'll focus on the animation parts of the applet and won't worry so much about how the threads work. We'll take another look at this applet later, after we've discussed threads in greater detail.
Listing 10.1. The DigitalClock applet.
1: import java.awt.Graphics; 2: import java.awt.Font; 3: import java.util.Date; 4: 5: public class DigitalClock extends java.applet.Applet 6: implements Runnable { 7: 8: Font theFont = new Font("TimesRoman",Font.BOLD,24); 9: Date theDate; 10: Thread runner; 11: 12: public void start() { 13: if (runner == null) { 14: runner = new Thread(this); 15: runner.start(); 16: } 17: } 18: 19: public void stop() { 20: if (runner != null) { 21: runner.stop(); 21: runner = null; 22: } 23: } 24: 25: public void run() { 26: while (true) { 27: theDate = new Date(); 28: repaint(); 29: try { Thread.sleep(1000); } 30: catch (InterruptedException e) { } 31: } 32: } 33: 34: public void paint(Graphics g) { 35: g.setFont(theFont); 36: g.drawString(theDate.toString(),10,50); 37: } 38:}
Analysis |
We'll look at this applet from the perspective of the actual animation parts in this section, and deal with the parts that manage threads later on. |
Lines 7 and 8 define two basic instance variables: theFont and theDate, which hold objects representing the current font and the current date, respectively. You'll learn more about these later.
The start() and stop() methods here start and stop a thread; the bulk of the applet's work goes on in the run() methods (lines 25 to 32).
Inside run() is where the animation actually takes place. Note the while loop inside this method (line 26); given that the test (true) always returns true, the loop never exits. A single animation frame is constructed inside that while loop, with the following steps:
On to the paint() method in lines 34 through 37. Here, inside paint(), all that happens is that the current font (in the variable theFont) is set, and the date itself is printed to the screen (note that you have to call the toString() method to convert the date to a string). Because paint() is called repeatedly with whatever value happens to be in theDate, the string is updated every second to reflect the new date.
There are a few things to note about this example. First, you might think it would be easier to create the new Date object inside the paint() method. That way you could use a local variable and not need an instance variable to pass the Date object around. Although doing things that way creates cleaner code, it also results in a less efficient program. The paint() method is called every time a frame needs to be changed. In this case, it's not that important, but in an animation that needs to change frames very quickly, the paint() method has to pause to create that new object every time. By leaving paint() to do what it does best-painting the screen-and calculating new objects beforehand, you can make painting as efficient as possible. This is precisely the same reason why the Font object is also in an instance variable.
So what are these threads all about? Why are they important to animation?
Threads are a very important part of Java and of programming Java. The larger your Java programs get and the more things they do, the more likely it is that you'll want to use threads. Depending on your experience with operating systems and with environments within those systems, you may or may not have run into the concept of threads, so let's start from the beginning.
First, the analogy. A group of students is on a bus, on a field trip somewhere. To pass the time, the teachers are leading a sing-along. As the trip progresses, the students sing one song, then when that song is done, they sing another song. While different parts of the bus could sing different songs, it wouldn't sound very good, so the singing of one song monopolizes the time until its done, at which time another song can start.
Now let's say you have two busses; both are on the same route to the field trip, both are going at the same speed, and both are full of students singing songs. But the songs being sung by the students in the second bus don't interfere with the songs being sung in the first bus; in this way you can get twice as many songs sung in the same amount of time by singing them in parallel.
Threads are like that. In a regular single-threaded program, the program starts executing, runs its initialization code, calls methods or procedures, and continues running and processing until it's complete or until the program is exited. That program runs in a single thread-it's the one bus with all the students.
Multithreading, as in Java, means that several different parts of the same program can run at the same time, in parallel, without interfering with each other. Multiple threads, each running by itself, are like multiple busses with different things going on in each bus.
Here's a simple example. Suppose you have a long computation near the start of a program's execution. This long computation may not be needed until later in the program's execution-it's actually tangential to the main point of the program, but it needs to get done eventually. In a single-threaded program, you have to wait for that computation to finish before the rest of the program can continue running. In a multithreaded system, you can put that computation into its own thread, and the rest of the program can continue to run independently.
Animation is an example of the kind of task that needs its own thread. Take, for example, that digital clock applet, which has an endless while() loop. If you didn't use threads, while() would run in the default Java system thread, which is also responsible for handling painting the screen, dealing with user input like mouse clicks, and keeping everything internally up- to-date. Unfortunately, however, if you run that while() loop in the main system thread, it will monopolize all Java's resources and prevent anything else-including painting-from happening. You'd never actually see anything on the screen because Java would be sitting and waiting for the while() loop to finish before it did anything else. And that's not what you want.
Using threads in Java, you can create parts of an applet (or application) that run in their own threads, and those parts will happily run all by themselves without interfering with anything else. Depending on how many threads you have, you may eventually tax the system so that all of them will run slower, but all of them will still run independently.
Even if you don't use lots of them, using threads in your applets is a good Java programming practice. The general rule of thumb for well-behaved applets: Whenever you have any bit of processing that is likely to continue for a long time (such as an animation loop, or a bit of code that takes a long time to execute), put it in a thread.
Creating applets that use threads is very easy. In fact, many of the basic things you need to do to use threads are just boilerplate code that you can copy and paste from one applet to another. Because it's so easy, there's almost no reason not to use threads in your applets, given the benefits.
There are four modifications you need to make to create an applet that uses threads:
The first change is to the first line of your class definition. You've already got something like this:
public class MyAppletClass extends java.applet.Applet { ... }
You need to change it to the following:
public class MyAppletClass extends java.applet.Applet implements Runnable { ... }
What does this do? It includes support for the Runnable interface in your applet. If you think way back to Section 2, "Object-Oriented Programming and Java," you'll remember that interfaces are a way to collect method names common to different classes, which can then be mixed in and implemented inside different classes that need to implement that behavior. Here, the Runnable interface defines the behavior your applet needs to run a thread; in particular, it gives you a default definition for the run() method. By implementing Runnable, you tell others that they can call the Run() method on your instances.
The second step is to add an instance variable to hold this applet's thread. Call it anything you like; it's a variable of the type Thread (Thread is a class in java.lang, so you don't have to import it):
Thread runner;
Third, add a start() method or modify the existing one so that it does nothing but create a new thread and start it running. Here's a typical example of a start() method:
public void start() { if (runner == null) { runner = new Thread(this); runner.start(); } }
If you modify start() to do nothing but spawn a thread, where does the code that drives your applet go? It goes into a new method, run(), which looks like this:
public void run() { // what your applet actually does }
Your run() method actually overrides the default version of run(), which you get when you include the Runnable interface with your applet. run() is one of those standard methods, like start() and paint(), that you override in your own classes to get standard behavior.
run() can contain anything you want to run in the separate thread: initialization code, the actual loop for your applet, or anything else that needs to run in its own thread. You also can create new objects and call methods from inside run(), and they'll also run inside that thread. The run() method is the real heart of your applet.
Finally, now that you've got threads running and a start() method to start them, you should add a stop() method to suspend execution of that thread (and therefore whatever the applet is doing at the time) when the reader leaves the page. stop(), like start(), is usually something along these lines:
public void stop() { if (runner != null) { runner.stop(); runner = null; } }
The stop() method here does two things: It stops the thread from executing and also sets the thread's variable runner to null. Setting the variable to null makes the Thread object it previously contained available for garbage collection so that the applet can be removed from memory after a certain amount of time. If the reader comes back to this page and this applet, the start() method creates a new thread and starts up the applet once again.
And that's it! Four basic modifications, and now you have a well-behaved applet that runs in its own thread.
Let's take another look at that DigitalClock applet, this time from the standpoint of threads. Listing 10.2 shows that applet's code once again.
Listing 10.2. The DigitalClock applet, revisited.
1: import java.awt.Graphics; 2: import java.awt.Font; 3: import java.util.Date; 4: 5: public class DigitalClock extends java.applet.Applet 6: implements Runnable { 7: 8: Font theFont = new Font("TimesRoman",Font.BOLD,24); 9: Date theDate; 10: Thread runner; 11: 12: public void start() { 13: if (runner == null) { 14: runner = new Thread(this); 15: runner.start(); 16: } 17: } 18: 19: public void stop() { 20: if (runner != null) { 21: runner.stop(); 21: runner = null; 22: } 23: } 24: 25: public void run() { 26: while (true) { 27: theDate = new Date(); 28: repaint(); 29: try { Thread.sleep(1000); } 30: catch (InterruptedException e) { } 31: } 32: } 33: 34: public void paint(Graphics g) { 35: g.setFont(theFont); 36: g.drawString(theDate.toString(),10,50); 37: } 38:}
Analysis |
Let's look at the lines of this applet that create and manage threads. First, look at the class definition itself in lines 5 and 6; note that the class definition includes the Runnable interface. Any classes you create that use threads must include Runnable. |
Line 10 defines a third instance variable for this class called runner of type Thread, which will hold the thread object for this applet.
Lines 12 through 23 define the boilerplate start() and stop() methods that do nothing except create and destroy threads. These method definitions can essentially be exactly the same from class to class because all they do is set up the infrastructure for the thread itself.
And, finally, the bulk of your applet's work goes on inside the run() method in lines 25 through 32, as we already discussed the last time we looked at this applet. Inside this method is the endless while loop, the calls to repaint(), and the sleep() method, which pauses things so they only run once a second.
If you've been following along with this lesson and trying the examples as you go, rather than reading this book on an airplane or in the bathtub, you may have noticed that when the digital clock program runs, every once in a while there's an annoying flicker in the animation. (Not that there's anything wrong with reading this book in the bathtub, but you won't see the flicker if you do that, so just trust me-there's a flicker.) This isn't a mistake or an error in the program; in fact, that flicker is a side effect of creating animation in Java. Because it is really annoying, you'll learn how to reduce flicker in this part of today's lesson so that your animations run cleaner and look better on the screen.
Flicker is caused by the way Java paints and repaints each frame of an applet. At the beginning of today's lesson, you learned that when you call the repaint() method, repaint() calls paint(). That's not precisely true. A call to paint() does indeed occur in response to a repaint(), but what actually happens are the following steps:
It's step 2, the call to update(), that causes animation flicker. Because the screen is cleared between frames, the parts of the screen that don't change alternate rapidly between being painted and being cleared. Hence, flickering.
There are two major ways to avoid flicker in your Java applets:
If the second way sounds complicated, that's because it is. Double-buffering involves drawing to an offscreen graphics surface and then copying that entire surface to the screen. Because it's more complicated, you'll explore that one tomorrow. Today let's cover the easier solution: overriding update().
The cause of flickering lies in the update() method. To reduce flickering, therefore, override update(). Here's what the default version of update() does (comes from the Component class, is part of the awt, and is one of the superclasses of the applet class. You'll learn more about it on Section 13, "Creating User Interfaces with the awt"):
public void update(Graphics g) { g.setColor(getBackground()); g.fillRect(0, 0, width, height); g.setColor(getForeground()); paint(g); }
Basically, update() clears the screen (or, to be exact, fills the applet's bounding rectangle with the background color), sets things back to normal, and then calls paint(). When you override update(), you have to keep these two things in mind and make sure that your version of update() does something similar. In the next two sections, you'll work through some examples of overriding update() in different cases to reduce flicker.
The first solution to reducing flicker is not to clear the screen at all. This works only for some applets, of course. Here's an example of an applet of this type. The ColorSwirl applet prints a single string to the screen ("All the Swirly Colors"), but that string is presented in different colors that fade into each other dynamically. This applet flickers terribly when it's run. Listing 10.3 shows the initial source for this applet, and Figure 10.2 shows the result.
Figure 10.2 : The ColorSwirl applet.
Listing 10.3. The ColorSwirl applet.
1: import java.awt.Graphics; 2: import java.awt.Color; 3: import java.awt.Font; 4: 5: public class ColorSwirl extends java.applet.Applet 6: implements Runnable { 7: 8: Font f = new Font("TimesRoman",Font.BOLD,48); 9: Color colors[] = new Color[50]; 10: Thread runThread; 11: 12: public void start() { 13: if (runThread == null) { 14: runThread = new Thread(this); 15: runThread.start(); 16: } 17: } 18: 19: public void stop() { 20: if (runThread != null) { 21: runThread.stop(); 22: runThread = null; 23: } 24: } 25: 26: public void run() { 27: 28: // initialize the color array 29: float c = 0; 30: for (int i = 0; i < colors.length; i++) { 31: colors[i] = 32: Color.getHSBColor(c, (float)1.0,(float)1.0); 33: c += .02; 34: } 35: 36: // cycle through the colors 37: int i = 0; 38: while (true) { 39: setForeground(colors[i]); 40: repaint(); 41: i++; 42: try { Thread.sleep(50); } 43: catch (InterruptedException e) { } 44: if (i == colors.length ) i = 0; 45: } 46: } 47: 48: public void paint(Graphics g) { 49: g.setFont(f); 50: g.drawString("All the Swirly Colors", 15, 50); 51: } 52: }]
Analysis |
There are three new things to note about this applet that might look strange to you: |
Now that you understand what the applet does, let's fix the flicker. Flicker here results because each time the applet is painted, there's a moment where the screen is cleared. Instead of the text cycling neatly from red to a nice pink to purple, it's going from red to gray, to pink to gray, to purple to gray, and so on-not very nice looking at all.
Because the screen clearing is all that's causing the problem, the solution is easy: Override update() and remove the part where the screen gets cleared. It doesn't really need to get cleared anyhow, because nothing is changing except the color of the text. With the screen clearing behavior removed from update(), all update needs to do is call paint(). Here's what the update() method looks like in this applet (you'll want to add it after the paint() method after line 51):
public void update(Graphics g) { paint(g); }
With that-with one small three-line addition-no more flicker.
Wasn't that easy?
Note |
If you're following along with the examples on the CD, the ColorSwirl.java file contains the original applet with the flicker; ColorSwirl2.java has the fixed version. |
For some applets, it won't be quite as easy as just not clearing the screen. With some kinds of animation, clearing the screen is necessary for the animation to work properly. Here's another example. In this applet, called Checkers, a red oval (a checker piece) moves from a black square to a white square, as if on a checkerboard. Listing 10.4 shows the code for this applet, and Figure 10.3 shows the applet itself.
Figure 10.3 : The Checkers applet.
Listing 10.4. The Checkers applet.
1: import java.awt.Graphics; 2: import java.awt.Color; 3: 4: public class Checkers extends java.applet.Applet 5: implements Runnable { 6: 7: Thread runner; 8: int xpos; 9: 10: public void start() { 11: if (runner == null) { 12: runner = new Thread(this); 13: runner.start(); 14: } 15: } 16: 17: public void stop() { 18: if (runner != null) { 19: runner.stop(); 20: runner = null; 21: } 22: } 23: 24: public void run() { 25: setBackground(Color.blue); 26: while (true) { 27: for (xpos = 5; xpos <= 105; xpos+=4) { 28: repaint(); 29: try { Thread.sleep(100); } 30: catch (InterruptedException e) { } 31: } 32: xpos = 5; 33: } 34: } 35: 36: public void paint(Graphics g) { 37: // Draw background 38: g.setColor(Color.black); 39: g.fillRect(0, 0, 100, 100); 40: g.setColor(Color.white); 41: g.fillRect(101, 0, 100, 100); 42: 43: // Draw checker 44: g.setColor(Color.red); 45: g.fillOval(xpos, 5, 90, 90); 46: } 47: }
Analysis |
Here's a quick run-through of what this applet does: An instance variable, xpos, keeps track of the current starting position of the checker (because it moves horizontally, the y stays constant and only the x changes; we don't need to keep track of the y position). In the run() method, you change the value of x and repaint, waiting 100 milliseconds between each move. The checker then appears to move from the left side of the screen to the right, resetting back at its original position once it hits the right side of the screen. |
In the actual paint() method, the background squares are painted (one black and one white), and then the checker is drawn at its current position.
This applet, like the ColorSwirl applet, also has a terrible flicker. (In line 25, I changed the background color to blue to emphasize it, so if you run this applet, you'll definitely see the flicker.)
However, the solution to solving the flicker problem for this applet is more difficult than for the last one, because you actually do want to clear the screen before the next frame is drawn. Otherwise, the red checker won't have the appearance of leaving one position and moving to another; it'll just leave a red smear from one side of the checkerboard to the other.
How do you get around this? You still clear the screen, in order to get the animation effect, but, rather than clearing the entire screen each time, you clear only the part that has actually changed from one frame to the next. By limiting the redraw to only a small area, you can eliminate some of the flicker you get from redrawing the entire screen.
To limit what gets redrawn, you need a couple things. First, you
need a way to restrict the drawing area so that each time paint()
is called, only the part that needs to get redrawn actually gets
redrawn. Fortunately, this is easy by using a mechanism called
clipping. Clipping, part
of the graphics class, enables you to restrict the drawing area
to a small portion of the full screen; although the entire screen
may get instructions to redraw, only the portions inside the clipping
area are actually drawn.
New Term |
Clipping restricts the drawing area to some smaller portion of the screen. |
The second thing you need is a way to keep track of the actual area to redraw. Both the left and right edges of the drawing area change for each frame of the animation (one side to draw the new oval, the other to erase the bit of the oval left over from the previous frame), so to keep track of those two x values, you need instance variables for both the left side and the right.
With those two concepts in mind, let's start modifying the Checkers applet to redraw only what needs to be redrawn. First, you'll add instance variables for the left and right edges of the drawing area. Let's call those instance variables ux1 and ux2 (u for update), where ux1 is the left side of the area to draw and ux2 the right:
int ux1,ux2;
Now let's modify the run() method so that it keeps track of the actual area to be drawn, which you would think is easy-just update each side for each iteration of the animation. Here, however, things can get complicated because of the way Java uses paint() and repaint().
The problem with updating the edges of the drawing area with each frame of the animation is that for every call to repaint() there may not be an individual corresponding paint(). If system resources get tight (because of other programs running on the system or for any other reason), paint() may not get executed immediately and several calls to paint() may queue up waiting for their turn to change the pixels on the screen. In this case, rather than trying to make all those calls to paint() in order (and be potentially behind all the time), Java catches up by executing only the most recent call to paint() and skips all the others.
This poses a difficult problem in the Checkers applet. If you update the edges of the drawing area with each call to repaint(), and a couple calls to paint() are skipped, you end up with bits of the drawing surface not being updated at all or bits of the oval (colloquially called "turds") left behind. Because of how repaint() and paint() work in Java, you cannot guarantee that every single clipping region will eventually get painted-some may be skipped. The way to solve this is not to reset the clipping region to something new every single pass, but instead to reset the region only if that region was indeed updated. This way, if a couple of calls to paint() get skipped, the area to be updated will get larger for each frame, and when paint() finally gets caught up, everything will get repainted correctly.
Yes, this is horrifyingly complex. If I could have written this applet more simply, I would have (and, in fact, I did make it as simple as I could after much rewriting), but without this mechanism the applet will not get repainted correctly (my first try at this applet left turds all over the place). Let's step through it slowly in the code so you can get a better grasp of what's going on at each step.
Let's start with run(), where each frame of the animation takes place. Here's where you calculate each side of the clipping area based on the old position of the oval and the new position of the oval. The value of ux1 (the left side of the drawing area) is the previous oval's x position (xpos), and the value of ux2 is the x position of the current oval plus the width of that oval (90 pixels in this example).
Here's what the old run() method looked like:
public void run() { setBackground(Color.blue); while (true) { for (xpos = 5; xpos <= 105; xpos += 4) { repaint(); try { Thread.sleep(100); } catch (InterruptedException e) { } } xpos = 5; } }
For each step in which the oval moves toward the right, you first update ux2 (the right edge of the drawing area):
ux2 = xpos + 90;
Then, after the repaint() has occurred, you can update ux1 to reflect the old x position of the oval. However, you want to update this value only if the paint actually happened, so you don't end up skipping bits of the screen. How can you tell if the paint actually happened? You can reset ux1 in paint() to a given value (say 0), and then test inside run() to see whether you can update that value or whether you have to wait for the paint() to occur:
if (ux1 == 0) ux1 = xpos;
Finally, there's one other change to make. When the oval reaches the right side of the screen and resets back to its original position, there's one frame where you want to redraw the whole screen rather than create a clipping region (otherwise, the image of the oval would remain on the right side of the screen). So, in this one case, you want to set ux2 to be the full width of the applet. Here we'll modify the line we just put in to set the value of ux2, using an if statement to test to see if the oval is at the left side of the screen:
if (xpos == 5) ux2 = size().width; else ux2 = xpos + 90;
The size() method is used to get the dimensions of the applet; size().width gives the full width of the applet so that the entire drawing surface will be updated.
Here's the new version of run() with those changes in place:
public void run() { setBackground(Color.blue); while (true) { for (xpos = 5; xpos <= 105; xpos+=4) { if (xpos == 5) ux2 = size().width; else ux2 = xpos + 90; repaint(); try { Thread.sleep(100); } catch (InterruptedException e) { } if (ux1 == 0) ux1 = xpos; } xpos = 5; } }
Those are the only modifications run() needs. Let's override update() to limit the region that is being painted to the left and right edges of the drawing area that you set inside run(). To clip the drawing area to a specific rectangle, use the clipRect() method. clipRect(), like drawRect(), fillRect(), and clearRect(), is defined for Graphics objects and takes four arguments: x and y starting positions, and the width and height of the region.
Here's where ux1 and ux2 come into play. ux1 is the x point of the top corner of the region; then use ux2 to get the width of the region by subtracting ux1 from that value. The y values are the standard y values for the oval, which don't vary at all (the oval starts at y position 5 and ends at 95). Finally, to finish update(), you call paint():
public void update(Graphics g) { g.clipRect(ux1, 5, ux2 - ux1, 95); paint(g); }
Note that with the clipping region in place, you don't have to do anything to the actual paint() method. paint() goes ahead and draws to the entire screen each time, but only the areas inside the clipping region actually get changed onscreen.
You will need to make one change to paint(), however. You need to update the trailing edge of each drawing area inside paint() in case several calls to paint() were skipped. Because you are testing for a value of 0 inside run(), inside paint() you can merely reset ux1 and ux2 to 0 after drawing everything:
ux1 = ux2 = 0;
Those are the only changes you have to make to this applet in order to draw only the parts of the applet that changed (and to manage the case where some frames don't get updated immediately). Although this doesn't totally eliminate flickering in the animation, it does reduce it a great deal. Try it and see. Listing 10.5 shows the final code for the Checkers applet (called Checkers2.java).
Listing 10.5. The final Checkers applet.
1: import java.awt.Graphics; 2: import java.awt.Color; 3: 4: public class Checkers2 extends java.applet.Applet implements Runnable { 5: 6: Thread runner; 7: int xpos; 8: int ux1,ux2; 9: 10: public void start() { 11: if (runner == null) { 12: runner = new Thread(this); 13: runner.start(); 14: } 15: } 16: 17: public void stop() { 18: if (runner != null) { 19: runner.stop(); 20: runner = null; 21: } 22: } 23: 24: public void run() { 25: setBackground(Color.blue); 26: while (true) { 27: for (xpos = 5; xpos <= 105; xpos+=4) { 28: if (xpos == 5) ux2 = size().width; 29: else ux2 = xpos + 90; 30: repaint(); 31: try { Thread.sleep(100); } 32: catch (InterruptedException e) { } 33: if (ux1 == 0) ux1 = xpos; 34: } 35: xpos = 5; 36: } 37: } 38: 39: public void update(Graphics g) { 40: g.clipRect(ux1, 5, ux2 - ux1, 95); 41: paint(g); 42: } 43: 44: public void paint(Graphics g) { 45: // Draw background 46: g.setColor(Color.black); 47: g.fillRect(0, 0, 100, 100); 48: g.setColor(Color.white); 49: g.fillRect(101, 0, 100, 100); 50: 51: // Draw checker 52: g.setColor(Color.red); 53: g.fillOval(xpos, 5, 90, 90); 54: 55: // reset the drawing area 56: ux1 = ux2 = 0; 57: } 58:}
Congratulations on getting through Section 10! This section was a bit rough; you've learned a lot, and it all might seem overwhelming. You learned about a plethora of methods to use and override-start(), stop(), paint(), repaint(), run(), and update()-and you got a basic foundation in creating and using threads. Other than handling bitmap images, which you'll learn about tomorrow, you now have the basic background to create just about any animation you want in Java.
Why all the indirection with paint(), repaint(), update(), and all that? Why not have a simple paint method that puts stuff on the screen when you want it there? | |
The Java awt enables you to nest drawable surfaces within other drawable surfaces. When a paint() takes place, all the parts of the system are redrawn, starting from the outermost surface and moving downward into the most nested one. Because the drawing of your applet takes place at the same time everything else is drawn, your applet doesn't get any special treatment. Your applet will be painted when everything else is painted. Although with this system you sacrifice some of the immediacy of instant painting, it enables your applet to coexist with the rest of the system more cleanly. | |
Are Java threads like threads on other systems? | |
Java threads have been influenced by other thread systems, and if you're used to working with threads, many of the concepts in Java threads will be very familiar to you. You learned the basics today; you'll learn more next week on Section 18. | |
When an applet uses threads, I just have to tell the thread to start and it starts, and tell it to stop and it stops? That's it? I don't have to test anything in my loops or keep track of its state? It just stops? | |
It just stops. When you put your applet into a thread, Java can control the execution of your applet much more readily. By causing the thread to stop, your applet just stops running, and then resumes when the thread starts up again. Yes, it's all automatic. Neat, isn't it? | |
The ColorSwirl applet seems to display only five or six colors, which isn't very swirly. What's going on here? | |
This is the same problem you ran into yestersection. On some systems, there might not be enough colors available to be able to display all of them reliably. If you're running into this problem, besides upgrading your hardware, you might try quitting other applications running on your system that use color. Other browsers or color tools in particular might be hogging colors that Java wants to be able to use. | |
Even with the changes you made, the Checkers applet still flickers. | |
And, unfortunately, it will continue to do so. Reducing the size of the drawing area by using clipping does reduce the flickering, but it doesn't stop it entirely. For many applets, using either of the methods described today may be enough to reduce animation flicker to the point where your applet looks good. To get totally flicker-free animation, you'll need to use a technique called double-buffering, which you'll learn about tomorrow. |
Download sample programs - Chekers.zip Checkers.zip ColorSwirl.zip DigitalClock.zip