5

So I am creating a basic application that I want to have a JLabel at the bottom of the screen that starts at the left bottom corner and moves, animation style, to the right bottom corner in a set time, and a static image in the center. To do this, I created a JFrame with a JPanel using BorderLayout. There is a JLabel with an ImageIcon added to BorderLayout.CENTER and a JPanel at BorderLayout.SOUTH. My code, while hastily written and far from pretty, is:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension; 
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.BorderFactory;

public class GameWindow extends JPanel{

private static JLabel mainWindow, arrowLabel, arrowBox;
protected static JFrame frame;
protected static JPanel arrows;

public static int x = 600;

public GameWindow(){
    mainWindow = new JLabel("Center");
    arrowLabel = new JLabel("Moving");
    arrows = new JPanel();
    arrows.setSize(600, 100);
    arrows.setLayout(null);
    arrowBox = new JLabel("");
    arrowBox.setBounds(0, 0, 150, 100);
    arrowBox.setPreferredSize(new Dimension(150, 100));
    arrowBox.setBorder(BorderFactory.createLineBorder(Color.black));
    arrows.add(arrowBox);
    this.setSize(600,600);
    this.setLayout(new BorderLayout());
    this.add(mainWindow, BorderLayout.CENTER);
    this.add(arrows, BorderLayout.SOUTH);
}

public static void main(String[] args)
{
    GameWindow g = new GameWindow();
    frame = new JFrame("Sword Sword Revolution");
    frame.add(g);
    frame.setSize(600,600);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setVisible(true);

    Timer t = new Timer(1000, new ActionListener(){

        @Override
        public void actionPerformed(ActionEvent e) {
            arrows.add(arrowLabel);
            arrowLabel.setBounds(x, 100, 100, 100);
            x-=50;
            arrows.repaint();
            frame.repaint();
        }

    });
    t.start();
}

}

The ImageIcon in the center JLabel appears fine, and the empty JLabel with a border appears at the bottom, but I cannot get the second JLabel with the arrow image to show up on screen. Eventually I will change to scheduleAtFixedRate to continuously move the JLabel, but right now I can't even get the image to appear on screen.

I also understand that I will most likely not be able to use FlowLayout for this, as I understand it does not allow you to set the location of your components. I tried using null layout, but with null layout the empty JLabel with a border does not appear. I can barely make out the top of the border at the bottom edge of the frame, but even with setLocation I cannot get it to appear where I want it to.

Obviously, my thought process is flawed, so any help would be appreciated.

Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
awestover89
  • 1,713
  • 4
  • 21
  • 37

3 Answers3

15

Your use of threading is all wrong for Swing applications. You should not be trying to add or remove components in a background thread but instead should use a Swing Timer to do this on the Swing event thread.

Also, what do you mean by:

I want to have a scrolling JLabel at the bottom of the screen

Please clarify the effect you're trying to achieve.

Also regarding,

I also understand that I will most likely not be able to use FlowLayout for this, as I understand it does not allow you to set the location of your components. I tried using null layout, but with null layout the empty JLabel with a border does not appear. I can barely make out the top of the border at the bottom edge of the frame, but even with setLocation I cannot get it to appear where I want it to.

No, don't use null layout for this situation. There are much better layout managers that can help you build your application in a cleaner more platform-independent manner.

Edit 3
Regarding:

To clarify, at the bottom of the screen I want a JLabel at the far right corner, then in the swing timer, the JLabel will gradually move to the left until it leaves the screen. If I could get setLocation to work, the basic premise would be to have a variable x set to 600, and then every second decrement x by say 50 and then redraw the JLabel at the new location on the screen. Basic animation.

I would create a JPanel for the bottom of the screen for the purposes of either holding your JLabel or displaying the image without a JLabel by overriding its paintComponent(...) method. If you use it as a container, then yes, its layout should be null, but the rest of the GUI should not be using null layout. The Swing Timer would simply change the JLabel's location and then call repaint() on its JPanel/container. If you go the latter route, you would draw the image in the JPanel's paintComponent(...) method using g.drawImage(myImage, x, y), and your timer would change x and/or y and call repaint() on the drawing JPanel.

Also, you likely do not want to keep adding a JLabel in your timer but rather simply moving the JLabel that's already displayed in the GUI.

Also, to avoid focus issues, don't use a KeyListener to capture keystroke input but rather use Key Bindings. Google will direct you to a great tutorial on this construct.

Edit 4
For example:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.EnumMap;

import javax.imageio.ImageIO;
import javax.swing.*;

@SuppressWarnings("serial")
public class AnimateExample extends JPanel {
   public static final String DUKE_IMG_PATH = 
         "https://duke.kenai.com/iconSized/duke.gif";
   private static final int PREF_W = 800;
   private static final int PREF_H = 800;
   private static final int TIMER_DELAY = 20;
   private static final String KEY_DOWN = "key down";
   private static final String KEY_RELEASE = "key release";
   public static final int TRANSLATE_SCALE = 3;
   private static final String BACKGROUND_STRING = "Use Arrow Keys to Move Image";
   private static final Font BG_STRING_FONT = new Font(Font.SANS_SERIF,
         Font.BOLD, 32);
   private EnumMap<Direction, Boolean> dirMap = 
         new EnumMap<AnimateExample.Direction, Boolean>(Direction.class);
   private BufferedImage image = null;
   private int imgX = 0;
   private int imgY = 0;
   private int bgStringX; 
   private int bgStringY; 

   public AnimateExample() {
      for (Direction dir : Direction.values()) {
         dirMap.put(dir, Boolean.FALSE);
      }
      try {
         URL imgUrl = new URL(DUKE_IMG_PATH);
         image = ImageIO.read(imgUrl);
      } catch (MalformedURLException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

      new Timer(TIMER_DELAY, new TimerListener()).start();

      // here we set up our key bindings
      int condition = JComponent.WHEN_IN_FOCUSED_WINDOW;
      InputMap inputMap = getInputMap(condition);
      ActionMap actionMap = getActionMap();
      for (final Direction dir : Direction.values()) {

         // for the key down key stroke
         KeyStroke keyStroke = KeyStroke.getKeyStroke(dir.getKeyCode(), 0,
               false);
         inputMap.put(keyStroke, dir.name() + KEY_DOWN);
         actionMap.put(dir.name() + KEY_DOWN, new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent arg0) {
               dirMap.put(dir, true);
            }
         });

         // for the key release key stroke
         keyStroke = KeyStroke.getKeyStroke(dir.getKeyCode(), 0, true);
         inputMap.put(keyStroke, dir.name() + KEY_RELEASE);
         actionMap.put(dir.name() + KEY_RELEASE, new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent arg0) {
               dirMap.put(dir, false);
            }
         });
      }

      FontMetrics fontMetrics = getFontMetrics(BG_STRING_FONT);
      int w = fontMetrics.stringWidth(BACKGROUND_STRING);
      int h = fontMetrics.getHeight();

      bgStringX = (PREF_W - w) / 2;
      bgStringY = (PREF_H - h) / 2;
   }

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

   @Override
   protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      Graphics2D g2 = (Graphics2D) g;
      g.setFont(BG_STRING_FONT);
      g.setColor(Color.LIGHT_GRAY);
      g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
            RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
      g.drawString(BACKGROUND_STRING, bgStringX, bgStringY);

      if (image != null) {
         g.drawImage(image, imgX, imgY, this);
      }
   }

   private class TimerListener implements ActionListener {
      public void actionPerformed(java.awt.event.ActionEvent e) {
         for (Direction dir : Direction.values()) {
            if (dirMap.get(dir)) {
               imgX += dir.getX() * TRANSLATE_SCALE;
               imgY += dir.getY() * TRANSLATE_SCALE;
            }
         }
         repaint();
      };
   }

   enum Direction {
      Up(KeyEvent.VK_UP, 0, -1), Down(KeyEvent.VK_DOWN, 0, 1), Left(
            KeyEvent.VK_LEFT, -1, 0), Right(KeyEvent.VK_RIGHT, 1, 0);

      private int keyCode;
      private int x;
      private int y;

      private Direction(int keyCode, int x, int y) {
         this.keyCode = keyCode;
         this.x = x;
         this.y = y;
      }

      public int getKeyCode() {
         return keyCode;
      }

      public int getX() {
         return x;
      }

      public int getY() {
         return y;
      }

   }

   private static void createAndShowGui() {
      AnimateExample mainPanel = new AnimateExample();

      JFrame frame = new JFrame("Animate Example");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(mainPanel);
      frame.pack();
      frame.setLocationByPlatform(true);
      frame.setVisible(true);
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}

Which will create this GUI:

enter image description here

Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
  • 1
    +1. And setting the location while using a LayoutManager has no effect . It is the LayoutManager which determines the location – Robin Sep 22 '12 at 16:49
  • I actually had the Timer before, but somebody told me it was better to use ScheduledExecutorService. Putting it back to using the Swing timer is no issue, I'll edit the question – awestover89 Sep 22 '12 at 16:50
  • If still not working, then create a simple compilable and runnable program that demonstrates your problem and that uses online images available to all, an [sscce](http://sscce.org). – Hovercraft Full Of Eels Sep 22 '12 at 16:51
  • To clarify, at the bottom of the screen I want a JLabel at the far right corner, then in the swing timer, the JLabel will gradually move to the left until it leaves the screen. If I could get setLocation to work, the basic premise would be to have a variable x set to 600, and then every second decrement x by say 50 and then redraw the JLabel at the new location on the screen. Basic animation. – awestover89 Sep 22 '12 at 17:01
  • 1
    @awestover89: please see edit 3 and **edit 4** for an example of what I'm talking about, both sscce-size and animation-wise. – Hovercraft Full Of Eels Sep 22 '12 at 17:39
  • I have never used many of those classes before, but with a little modification that 4th edit did the trick perfectly. Looking it over, for my purposes I think I can actually get rid of the EnumMap stuff, the image will always be scrolling without any input from the user, so instead of the foreach loop in the actionPerformed function of TimerListener, I just did: `imgX - 1; repaint();` and the image appears and moves across the screen. Thanks a bunch – awestover89 Sep 22 '12 at 20:08
  • @awestover: yes, the EnumMap is only for tying animation with the key bindings so that the animation's movements can be driven by arrow key presses. If you don't need that functionality, then you don't need the enum or the Map. Best of luck! – Hovercraft Full Of Eels Sep 22 '12 at 20:12
8

Three possibilities:

  • You can either use a library like SlidingLayout to create such transition with very few lines of code. You won't be able to tweak the animation but your life will be easier.

  • You can use an animation engine like the Universal Tween Engine to configure everything by hand and tweak the animation as much as you want (the first lib uses this one under the hood). Using such engine, you can animate what you want: positions, colors, font size, ...

  • You can code everything by hand and embrace the hell that is the animation world :)

In the end, you'll be able to quickly create animations like these (it's a tool I'm currently working on, used to configure eclipse projects for android dev using the LibGDX game framework):
enter image description here enter image description here

I made these libraries to ease the pain that is UI animation (I love UI design :p). I released them open-source (free to use, license apache-2), hoping they may help some people too.

If you need help, there is a dedicated help forum for each library.

Aurelien Ribon
  • 7,548
  • 3
  • 43
  • 54
  • 4
    I'll look into that. Back when I was a student I hated the idea of using external libraries. Something to do with pride and wanting to say I did it all myself. I have, however, learned to embrace existing libraries in my professional career, the whole not reinventing the wheel thing. I'll let you know, thanks – awestover89 Sep 22 '12 at 17:20
1

Very simple, look at this:

javax.swing.JLabel lb = new javax.swing.JLabel();
Image image = Toolkit.getDefaultToolkit().createImage("Your animated GIF");
ImageIcon xIcon = new ImageIcon(image);
xIcon.setImageObserver(this);
lb.setIcon(xIcon);