简体   繁体   中英

How to animate with java awt

I'm trying to make an animation of a red oval that will move to the right of the screen. But it just draws the oval. I don't know what I'm doing wrong and I literally can't find anything about how to do this. Any help would be awesome, thanks.

import java.awt.*;  

public class mainClass  
{    
    public mainClass()    
    {    

        Frame f = new Frame("Canvas Example");   
        f.add(new MyCanvas());    

        f.setLayout(null);    
        f.setSize(400, 400);   
        f.setVisible(true);    
    }    

    public static void main(String args[])    
    {    
        new mainClass();
    }    
}    


class MyCanvas extends Canvas    
{    
    int x = 75;
    public MyCanvas() {    
        setBackground (Color.BLACK);    
        setSize(400, 400);    
    }    

    public void paint(Graphics g)    
    {     
        g.setColor(Color.red);    
        g.fillOval(x, 75, 150, 75);
    }
    public void update(Graphics g)    
    {     
        x++;
    }  
}     

The reason this doesn't animate is that nothing triggers the component to update and repaint itself. There are a few things that need to be considered:

  1. Something needs to call the update method. Ordinarily, this is triggered by a call to repaint() on the component, but nothing in this code calls that method.

  2. It's important for an overridden update method to call super.update(g) to ensure the default behavior is invoked (clearing the canvas and painting it again).

  3. Animation has a time component: the oval should move over some period of time. This needs to be incorporated into the logic. AWT has no built-in mechanism for timed behavior.

    If you're able to use classes from Swing, the javax.swing.Timer class is very useful for animation. It executes your callback on the AWT thread, and therefore means that you don't have to take special measures to ensure thread safety.

    If you can't use Swing, it can use java.util.Timer or a custom thread, but will need to manage thread synchronization directly.

  4. You'll probably also want the animation to stop once the oval reaches the edge of the canvas.

Here's an example using javax.swing.Timer (assuming Java 8 or later). Note that all of the animation logic is in the ActionListener attached to the Timer , so the overridden update method has been removed:

import javax.swing.*;
import java.awt.*;

public class MainClass {
    public static final int CANVAS_SIZE = 400;

    public MainClass() {

        Frame f = new Frame("Canvas Example");
        f.add(new MyCanvas(CANVAS_SIZE));

        f.setLayout(null);
        f.setSize(CANVAS_SIZE, CANVAS_SIZE);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        new MainClass();
    }
}


class MyCanvas extends Canvas {
    public static final int INITIAL_POSITION = 75;
    public static final int HEIGHT = 75;
    public static final int WIDTH = 150;

    private static final int TIMER_DELAY_MILLIS = 1000 / 30; // 30 FPS

    private int x = INITIAL_POSITION;
    private final Timer timer;

    public MyCanvas(int canvasSize) {
        setBackground(Color.BLACK);
        setSize(canvasSize, canvasSize);

        timer = new Timer(TIMER_DELAY_MILLIS, (event) -> {
            // ensure the oval stays on the canvas
            if (x + WIDTH < getWidth())  {
                x++;
                repaint();
            } else {
                stopAnimation();
            }
        });
        timer.start();
    }

    public void paint(Graphics g) {
        g.setColor(Color.red);
        g.fillOval(x, INITIAL_POSITION, WIDTH, HEIGHT);
    }

    private void stopAnimation() {
        timer.stop();
    }
}

This code has a few additional incidental changes.

  • Updated the name of mainClass to MainClass (leading capital "M") to comply with standard Java naming conventions.
  • Changed String args[] to String[] args for the same reason.
  • Extracted numeric constants to named static final fields.
  • Made the canvas size a constructor parameter, controlled by the caller.
  • Made x private.
  • Minor formatting changes to ensure a consistent style.

One option that doesn't use javax.swing.Timer (with unchanged code omitted):

private final AtomicInteger x = new AtomicInteger(INITIAL_POSITION);

public MyCanvas(int canvasSize) {
    setBackground(Color.BLACK);
    setSize(canvasSize, canvasSize);

    new Thread(() -> {
        try {
            // ensure the oval stays on the canvas
            while (x.incrementAndGet() + WIDTH < getWidth())  {
                Thread.sleep(TIMER_DELAY_MILLIS);
                repaint();
            }
        } catch (InterruptedException e) {
            // Just let the thread exit
            Thread.currentThread().interrupt();
        }
    }).start();
}

Theory

Animation is hard, I mean, really good animation is hard. There is a not of theory which goes into creating good animation, things like easement, anticipation, squish... I could go on, but I'm boring myself.

The point is, simply incrementing a value (AKA linear progression) is a poor approach to animation. If the system is slow, busy or for some other reason isn't keeping up, the animation will suffer because it (stuttering, pauses, etc).

A "better" solution is to use a time based progression. That is, you specify the amount of time it will take to move from the current state to it's new state and the continuously loop and update the state until you run out of time.

The "main loop"

If you do any research into game development, they always talk about this thing called the "main loop".

The "main loop" is responsible for updating the game state and scheduling paint passes.

In terms to your question, you need a "main loop" which can update the position of the oval until it reaches it's target position.

Because most GUI frameworks are already running within their own thread context, you need to setup your "main loop" in another thread

AWT

Some theory

AWT is the original GUI framework, so it's "old". While Swing does sit on top of it, you'll find more people have experience with Swing then they do AWT.

One of the important things to keep in mind is, Canvas is not double buffered, so, if you're updating the component fast enough, it will flash.

To overcome this, you need to implement some kind of double buffering workflow.

Runnable example

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.time.Instant;

public class Test {
    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                Frame frame = new Frame();
                frame.add(new TestCanvas());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class Ticker implements Runnable {

        public interface Callbck {
            public void didTick(Ticker ticker);
        }

        private boolean isRunning = false;
        private Thread thread;

        private Callbck callback;

        public void setCallback(Callbck tick) {
            this.callback = tick;
        }

        public void start() {
            if (isRunning) {
                return;
            }
            isRunning = true;
            thread = new Thread(this);
            thread.setDaemon(false);
            thread.start();
        }

        public void stop() {
            if (!isRunning) {
                return;
            }
            isRunning = false;
            thread.interrupt();
            thread = null;
        }

        @Override
        public void run() {
            while (isRunning) {
                try {
                    Thread.sleep(5);
                    if (callback != null) {
                        callback.didTick(this);
                    }
                } catch (InterruptedException ex) {
                    isRunning = false;
                }
            }
        }

    }

    public class TestCanvas extends Canvas {

        private BufferedImage buffer;

        int posX;

        private Ticker ticker;
        private Instant startedAt;
        private Duration duration = Duration.ofSeconds(5);

        public TestCanvas() {
            ticker = new Ticker();
            ticker.setCallback(new Ticker.Callbck() {
                @Override
                public void didTick(Ticker ticker) {
                    if (startedAt == null) {
                        startedAt = Instant.now();
                    }
                    Duration runtime = Duration.between(startedAt, Instant.now());
                    double progress = runtime.toMillis() / (double)duration.toMillis();

                    if (progress >= 1.0) {
                        stopAnimation();
                    }

                    posX = (int)(getWidth() * progress);
                    repaint();
                }
            });
        }

        protected void startAnimtion() {
            ticker.start();
        }

        protected void stopAnimation() {
            ticker.stop();
        }

        @Override
        public void setBounds(int x, int y, int width, int height) {
            buffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            super.setBounds(x, y, width, height);
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        public void addNotify() {
            super.addNotify();
            startAnimtion();
        }

        @Override
        public void removeNotify() {
            super.removeNotify();
            buffer = null;
        }

        @Override
        public void paint(Graphics g) {
            super.paint(g);
            if (buffer == null) {
                return;
            }
            Graphics2D g2d = buffer.createGraphics();
            g2d.setColor(getBackground());
            g2d.fillRect(0, 0, getWidth(), getHeight());
            g2d.setColor(Color.RED);
            int midY = getHeight() / 2;
            g2d.fillOval(posX, midY - 5, 10, 10);
            g2d.dispose();

            g.drawImage(buffer, 0, 0, this);
        }

    }
}

What is Canvas go for...?

In most cases, you should avoid using Canvas , for many of the reasons mentioned above, but one of the reasons you might consider using Canvas is if you want to take full control over the painting process. You might do this if you want to create a complex game which and you want to get the best possible performance out of the rendering pipeline.

See BufferStrategy and BufferCapabilities and the JavaDocs for more detail

A Swing based implementation

Hopefully I've convinced you that a Swing implementation might be a better solution, which in that case you should make use of a Swing Timer instead of Thread , as Swing is not thread safe

See Concurrency in Swing andHow to Use Swing Timers for more details

Runnable example

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.Instant;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class Ticker {

        public interface Callbck {
            public void didTick(Ticker ticker);
        }

        private Timer timer;

        private Callbck callback;

        public void setCallback(Callbck tick) {
            this.callback = tick;
        }

        public void start() {
            if (timer != null) {
                return;
            }
            timer = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (callback == null) {
                        return;
                    }
                    callback.didTick(Ticker.this);
                }
            });
            timer.start();
        }

        public void stop() {
            if (timer == null) {
                return;
            }
            timer.stop();
            timer = null;
        }

    }

    public class TestPane extends JPanel {

        int posX;

        private Ticker ticker;
        private Instant startedAt;
        private Duration duration = Duration.ofSeconds(5);

        public TestPane() {
            ticker = new Ticker();
            ticker.setCallback(new Ticker.Callbck() {
                @Override
                public void didTick(Ticker ticker) {
                    if (startedAt == null) {
                        startedAt = Instant.now();
                    }
                    Duration runtime = Duration.between(startedAt, Instant.now());
                    double progress = runtime.toMillis() / (double) duration.toMillis();

                    if (progress >= 1.0) {
                        stopAnimation();
                    }

                    posX = (int) (getWidth() * progress);
                    repaint();
                }
            });
        }

        protected void startAnimtion() {
            ticker.start();
        }

        protected void stopAnimation() {
            ticker.stop();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        public void addNotify() {
            super.addNotify();
            startAnimtion();
        }

        @Override
        public void removeNotify() {
            super.removeNotify();
            stopAnimation();
        }

        @Override
        public void paint(Graphics g) {
            super.paint(g);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setColor(Color.RED);
            int midY = getHeight() / 2;
            g2d.fillOval(posX, midY - 5, 10, 10);
            g2d.dispose();
        }

    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM