Santiago Rosales
Santiago Rosales

Reputation: 79

Animate image list by group Java Swing

Currently my code animates an image from the list from initial size to final size and continues with the next one until it completes animating all images and all have their final size.

package ui;

import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.Timer;

import utils.SudokuGen;

// TODO: Auto-generated Javadoc
/**
 * The Class ButtonPanel.
 */
public class ButtonPanel extends JPanel implements ActionListener{

    /** The Constant serialVersionUID. */
    private static final long serialVersionUID = 1L;

    /** The buttons. */
    private JButton[] buttons;

    private int[] imgsize;
    private int index,count;
    private Timer timer;
    private ImageButton images;
    private static final int COUNT = 10;
    private static final int INITSIZE = 32;
    private static final int COLUMNS = 1;
    private static final int ROWS = 9;

    /**
     * Instantiates a new button panel.
     *
     * @param puzzle The Sudoku matrix object
     * @param images The images object
     * @param sPanel The Sudoku JPanel
     */
    public ButtonPanel(SudokuGen puzzle, ImageButton images, SudokuPanel sPanel){

        //Create an array of JButton with 9 elements
        this.buttons = new JButton[puzzle.getMyboard().getNewnumbers().size()];

        this.images = images;
        this.imgsize = new int[COUNT];
        for (int i=0;i<COUNT;i++){
            imgsize[i] = INITSIZE;
        }
        //Sets the layout to a 9*1 GridLayout with padding 5
        this.setLayout(new GridLayout(ROWS,COLUMNS,5,5));

        //Load an image, create the MouseListener and add it at index i
        for (int i=1;i<this.images.getImagelist().size();i++){
            ImageIcon image = new ImageIcon(this.images.getImagelist()
                    .get(i).getImage().getScaledInstance(32, 32, Image.SCALE_SMOOTH));
            buttons[i] = createMouseListener(image, i, puzzle, sPanel);
            buttons[i].setPreferredSize(new Dimension(100, 100));
            this.add(buttons[i]);
        }

        index = 1; //first button
        count = 0; //times to resize the button

        timer = new Timer(5,this);
        timer.start();  
    }

    public void set(int X){
        this.imgsize[index] = X;
    }

    /**
     * Creates the mouse listener.
     *
     * @param image The image at index i
     * @param i The index for the button i
     * @param puzzle The Sudoku matrix object
     * @param sPanel The Sudoku JPanel
     * @return the JButton to add at index i
     */
    private JButton createMouseListener(ImageIcon image, int i, SudokuGen puzzle, SudokuPanel sPanel){
        JButton button = new JButton();
        button.setIcon(image);
        button.setActionCommand(Integer.toString(puzzle.getMyboard().getNewnumbers().get(i)));
        button.addActionListener(sPanel.new ButtonActionListener());
        return button;      
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        count++;
        set(this.imgsize[index]+1); //increase the size of the image

        if (this.imgsize[index] < 64){

            ImageIcon image = new ImageIcon(this.images.getImagelist()
                .get(index).getImage().getScaledInstance(this.imgsize[index], this.imgsize[index], Image.SCALE_SMOOTH));
            buttons[index].setIcon(image);
            if (count > 24){ //reached final size
                count = 0; //start counting again
                index++; //move to the next image
            }   
        };
        if (index == 10) timer.stop();
        repaint();

    }

}

I'm not able to provide an SSCCE given the amount of resources needed for it to work so apologizes for that.

My goal is to start animating the next image when the first animation finished half its process. So when the second image reaches its half animation the first one completes its full animation. Kind of a wave effect.

If there something else I could improve, I'm glad to hear suggestions.

Update: Updated method used to achieve the effect:

public void actionPerformed(ActionEvent e) {

    count++;
    this.imgsize[index]++;

        ImageIcon image = new ImageIcon(this.images.getImagelist()
            .get(index).getImage().getScaledInstance(this.imgsize[index], this.imgsize[index], Image.SCALE_SMOOTH));
        buttons[index].setIcon(image);
        if (count > 24){
            count = 0;
            index++;
        }   
    if (count > 12 && index < 9)
    {
        this.imgsize[index+1]++;
        image = new ImageIcon(this.images.getImagelist()
                .get(index+1).getImage().getScaledInstance(this.imgsize[index+1], this.imgsize[index+1], Image.SCALE_SMOOTH));
            buttons[index+1].setIcon(image);
    }
    if (index == 10) timer.stop();
    repaint();

}

Upvotes: 2

Views: 466

Answers (1)

MadProgrammer
MadProgrammer

Reputation: 347234

This is a somewhat overly simplified idea...

Basically, it sets up a normalised timeline (0-1) where events can occur along it, starting and ending at specified points. During these "periods" some "action" can occur.

In your case, these actions are image scaling actions, the scale of which is a proportion of the amount of time that a given event has been running, which is a proportion of the overall time line.

This all sounds wonderfully confusing, but the benefit of which is, you can change the duration of the timeline and the rest of the code will scale automatically

For example, you could take...

timeline = new Timeline(events, 10000);

which generates a timeline to run over 10 seconds and change it to

timeline = new Timeline(events, 5000);

which will generate a timeline to run over 5 seconds and you've not had to change a single line of code to make it work.

I've also spend some time working out how to automatically generate timeline events from a series of images, so you can supply n number of images, and it will generate the required events so that they overlap each other accordingly

Example

Now, you're probably thinking that this is all overly complicated, but the point is, it's all variable, the duration over which the images are scaled, the number of images, it's all variable and you don't need to do much to add/remove images or change the duration

Cavert- I've used getScaledInstance for this example, it's neither fast nor does it generate a quality image

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

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

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

                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        });
    }

    public class TestPane extends JPanel {

        private Timeline timeline;

        private Map<Object, Image> mapImages = new HashMap<>(5);
        private List<Object> imageOrder = new ArrayList<>(5);

        public TestPane() throws IOException {
            BufferedImage[] images = new BufferedImage[]{
                ImageIO.read(new File("...")),
                ImageIO.read(new File("...")),
                ImageIO.read(new File("...")),
                ImageIO.read(new File("..."))
            };
            List<TimelineEvent> events = generateEvents(images, new ImageScaledObserver() {
                @Override
                public void imageScaled(Object id, Image image) {
                    if (!imageOrder.contains(id)) {
                        imageOrder.add(id);
                    }
                    mapImages.put(id, image);
                    repaint();
                }
            });

            timeline = new Timeline(events, 10000);
            Timer timer = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (timeline.update()) {
                        ((Timer) e.getSource()).stop();
                    }
                }
            });
            timer.setInitialDelay(5000);
            timer.start();
        }

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

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            for (Object id : imageOrder) {
                Image image = mapImages.get(id);
                int x = (getWidth() - image.getWidth(this)) / 2;
                int y = (getHeight() - image.getHeight(this)) / 2;
                g2d.drawImage(image, x, y, this);
            }
            g2d.dispose();
        }

    }

    protected List<TimelineEvent> generateEvents(BufferedImage[] images, ImageScaledObserver observer) {
        double length = 1.0 / (double) (images.length);
        double overlap = length * 0.5;
        List<TimelineEvent> events = new ArrayList<>(images.length);
        double startAt = 0.0;
        for (BufferedImage image : images) {
            double endAt = Math.min(startAt + length + (overlap / 2.0), 1.0);
            events.add(new ScaleImageTimelineEvent(image, observer, startAt, endAt));
            startAt = Math.min(endAt - (overlap / 2.0), 1.0);
        }
        return events;
    }

    public interface TimelineEvent {

        public double[] range();

        public boolean isWithin(double timelineProgression);

        public void performAction(double timelineProgression);
    }

    public abstract class AbstractTimelineEvent implements TimelineEvent {

        private double from, to;

        public AbstractTimelineEvent(double from, double to) {
            this.from = from;
            this.to = to;
        }

        @Override
        public double[] range() {
            return new double[]{from, to};
        }

        @Override
        public boolean isWithin(double timelineProgression) {
            boolean within = timelineProgression >= from && timelineProgression <= to;
            return within;
        }

        protected double localisedProgression(double timelineProgression) {
            double max = from - to;
            double value = timelineProgression - to;
            double weight = value / max;
            return 1d - weight;
        }

    }

    public interface ImageScaledObserver {

        public void imageScaled(Object id, Image img);
    }

    public class ScaleImageTimelineEvent extends AbstractTimelineEvent {

        private BufferedImage original;
        private ImageScaledObserver observer;
        private UUID id;

        public ScaleImageTimelineEvent(BufferedImage image, ImageScaledObserver observer, double from, double to) {
            super(from, to);
            this.original = image;
            this.observer = observer;
            this.id = UUID.randomUUID();
        }

        @Override
        public void performAction(double timelineProgression) {
            double progress = localisedProgression(timelineProgression);
            Image image = null;
            if (progress < 1.0) {
                int width = (int) (original.getWidth() * progress);
                if (width > 0) {
                    image = original.getScaledInstance((int) (original.getWidth() * progress), -1, Image.SCALE_FAST);
                }
            } else {
                image = original;
            }
            if (image != null) {
                observer.imageScaled(id, image);
            }
        }

    }

    public static class Timeline {

        private List<TimelineEvent> events;

        private Long startTime;
        private long duration;

        public Timeline(List<TimelineEvent> events, long duration) {
            this.events = events;
            this.duration = duration;
        }

        public List<TimelineEvent> getEvents() {
            return events;
        }

        public Long getStartTime() {
            return startTime;
        }

        public long getDuration() {
            return duration;
        }

        public void start() {
            if (startTime == null) {
                startTime = System.nanoTime();
            }
        }

        public void stop() {
            startTime = null;
        }

        public boolean update() {
            if (startTime == null) {
                start();
            }

            boolean completed = false;

            long currentTime = System.nanoTime();
            long diff = currentTime - getStartTime();

            long nanoDuration = TimeUnit.NANOSECONDS.convert(getDuration(), TimeUnit.MILLISECONDS);
            double progress = diff / (double) nanoDuration;
            if (progress > 1.0d) {
                progress = 1.0d;
                completed = true;
                stop();
            }

            for (TimelineEvent evt : getEvents()) {
                if (evt.isWithin(progress)) {
                    evt.performAction(progress);
                }
            }
            return completed;
        }
    }

}

I'd also suggest having a look at some of the animation frameworks which are available, which provide additional functionality, like easement. Maybe do a search for "Timing Framework" as a suggestion

The example uses a UUID to generate a unique identifier for the imageScaled event. I wouldn't be hard to make ScaleImageTimelineEvent take an identifier of your own which is linked back to a specific image, allowing you to determine which image was scaled and perform more accurate updates

Upvotes: 2

Related Questions