I originally had an answer about Key Bindings, but after a little testing I found that they still had the same stutter problem.

Don't rely on the repeat rate of the OS. It can be different for every platform and a user may also customize it.

Instead use a Timer to schedule the event. You start the Timer on a keyPressed and stop the Timer on keyReleased.

import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.util.Map;
import java.util.HashMap;
import javax.imageio.ImageIO;
import javax.swing.*;

public class KeyboardAnimation implements ActionListener
{
    private final static String PRESSED = "pressed ";
    private final static String RELEASED = "released ";
    private final static Point RELEASED_POINT = new Point(0, 0);

    private JComponent component;
    private Timer timer;
    private Map<String, Point> pressedKeys = new HashMap<String, Point>();

    public KeyboardAnimation(JComponent component, int delay)
    {
        this.component = component;

        timer = new Timer(delay, this);
        timer.setInitialDelay( 0 );
    }

    public void addAction(String keyStroke, int deltaX, int deltaY)
    {
//      InputMap inputMap = component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        InputMap inputMap = component.getInputMap();
        ActionMap actionMap = component.getActionMap();

        String pressedKey = PRESSED + keyStroke;
        KeyStroke pressedKeyStroke = KeyStroke.getKeyStroke( pressedKey );
        Action pressedAction = new AnimationAction(keyStroke, new Point(deltaX, deltaY));
        inputMap.put(pressedKeyStroke, pressedKey);
        actionMap.put(pressedKey, pressedAction);

        String releasedKey = RELEASED + keyStroke;
        KeyStroke releasedKeyStroke = KeyStroke.getKeyStroke( releasedKey );
        Action releasedAction = new AnimationAction(keyStroke, RELEASED_POINT);
        inputMap.put(releasedKeyStroke, releasedKey);
        actionMap.put(releasedKey, releasedAction);
    }

    private void handleKeyEvent(String keyStroke, Point moveDelta)
    {
        //  Keep track of which keys are pressed

        if (RELEASED_POINT == moveDelta)
            pressedKeys.remove( keyStroke );
        else
            pressedKeys.put(keyStroke, moveDelta);

        //  Start the Timer when the first key is pressed

        if (pressedKeys.size() == 1)
        {
            timer.start();
        }

        //  Stop the Timer when all keys have been released

        if (pressedKeys.size() == 0)
        {
            timer.stop();
        }
    }

    //  Invoked when the Timer fires

    public void actionPerformed(ActionEvent e)
    {
        moveComponent();
    }

    //  Move the component to its new location

    private void moveComponent()
    {
        int componentWidth = component.getSize().width;
        int componentHeight = component.getSize().height;

        Dimension parentSize = component.getParent().getSize();
        int parentWidth  = parentSize.width;
        int parentHeight = parentSize.height;

        //  Calculate new move

        int deltaX = 0;
        int deltaY = 0;

        for (Point delta : pressedKeys.values())
        {
            deltaX += delta.x;
            deltaY += delta.y;
        }


        //  Determine next X position

        int nextX = Math.max(component.getLocation().x + deltaX, 0);

        if ( nextX + componentWidth > parentWidth)
        {
            nextX = parentWidth - componentWidth;
        }

        //  Determine next Y position

        int nextY = Math.max(component.getLocation().y + deltaY, 0);

        if ( nextY + componentHeight > parentHeight)
        {
            nextY = parentHeight - componentHeight;
        }

        //  Move the component

        component.setLocation(nextX, nextY);
    }

    private class AnimationAction extends AbstractAction implements ActionListener
    {
        private Point moveDelta;

        public AnimationAction(String keyStroke, Point moveDelta)
        {
            super(PRESSED + keyStroke);
            putValue(ACTION_COMMAND_KEY, keyStroke);

            this.moveDelta = moveDelta;
        }

        public void actionPerformed(ActionEvent e)
        {
            handleKeyEvent((String)getValue(ACTION_COMMAND_KEY), moveDelta);
        }
    }

    public static void main(String[] args)
    {
        JPanel contentPane = new JPanel();
        contentPane.setLayout( null );

        Icon dukeIcon = null;

        try
        {
            dukeIcon = new ImageIcon( "dukewavered.gif" );
//          dukeIcon = new ImageIcon( ImageIO.read( new URL("http://duke.kenai.com/iconSized/duke4.gif") ) );
        }
        catch(Exception e)
        {
            System.out.println(e);
        }

        JLabel duke = new JLabel( dukeIcon );
        duke.setSize( duke.getPreferredSize() );
        duke.setLocation(100, 100);
        contentPane.add( duke );

        KeyboardAnimation navigation = new KeyboardAnimation(duke, 24);
        navigation.addAction("LEFT", -3,  0);
        navigation.addAction("RIGHT", 3,  0);
        navigation.addAction("UP",    0, -3);
        navigation.addAction("DOWN",  0,  3);

        navigation.addAction("A", -5,  0);
        navigation.addAction("S",  5,  0);
        navigation.addAction("Z",  0, -5);
        navigation.addAction("X",  0,  5);
        navigation.addAction("V",  5,  5);

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
//      frame.getContentPane().add(new JTextField(), BorderLayout.SOUTH);
        frame.getContentPane().add(contentPane);
        frame.setSize(600, 600);
        frame.setLocationRelativeTo( null );
        frame.setVisible(true);
    }

}

This code was tested on Windows where the order of events is keyPressed, keyPressed, keyPressed... keyReleased.

However, I think on a Mac (or Unix) the order of events is keyPressed, keyReleased, keyPressed, keyReleased... So I'm not sure if this code would works any better than your current code.


  1. You should use Key Bindings as they solve the majority of focus related issues and are generally more flexible...
  2. You need to define a flag to indicate when a key is pressed. While pressed, you should not perform any additional tasks...

For examples...

  • Java object movement
  • Using keypad to move a circle at angles in java
  • Problems with Java's Paint method, ridiculous refresh velocity which shows a mechanism for accelerating an object while a key is pressed ;)

Updated with simple example

In most games, you should be reacting to "state changes" rather then actual key events. This means that the event that actually changes the state can be variable (think custom keys)

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class SinglePressKeyBinding {

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

    public SinglePressKeyBinding() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private JLabel message;

        private boolean spacedOut = false;

        public TestPane() {
            message = new JLabel("Waiting");
            setLayout(new GridBagLayout());
            add(message);

            InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            ActionMap am = getActionMap();

            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, false), "space-pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, true), "space-released");

            am.put("space-pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (spacedOut) {
                        message.setText("I'm ignoring you");
                    } else {
                        spacedOut = true;
                        message.setText("Spaced out");
                    }
                }
            });
            am.put("space-released", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    spacedOut = false;
                    message.setText("Back to earth");
                }
            });

        }

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

    }
}

A good idea is to set boolean values for what keys you want to track, and then on the keypressed event activate one of the booleans, and on key released, deactivate it. It will remove the lag of the keys and allow multiple key presses too!