如何使我的“缓冲图像”类显示在GUI中?

时间:2018-09-04 08:27:35

标签: java graphics timer bufferedimage

我有一个程序,使用计时器切换图像来制作动画。当程序在其最后一张图像上时,我使用一个类来创建该图像的缓冲图像,并在其上添加文本。当显示动画的最后一个图像时,我想将显示的图像更改为缓冲的图像。我无法正常工作。原来的代码就像没有加粗的部分一样播放。如果我删除其上方的行,它将显示带有文本的图像,并且没有其他显示。我应该对代码进行哪些修改以解决此问题?

执行动画的类

browser.newPage().then(Page => 
{ //maybe set some context here so that the page can access the context from sessionStorage
  page.goto(url).then( .....)})

将文字放在图片上的类

**import java.awt.event.*;
  import java.awt.Graphics;
  import java.awt.Color;
  import java.awt.Font;
  import java.awt.image.*;

  import java.io.*;
  import java.io.File;

  import java.awt.*;
  import java.awt.image.BufferedImage;

  import java.net.URL;

  import javax.swing.*;
  import javax.swing.*;

  import javax.imageio.ImageIO;

  /**
   * Write a description of class Reveal here.
   *
   * @author (your name)
   * @version (a version number or a date)
   */
  public class Reveal extends JPanel
  {
      private JPanel panel = new JPanel();       //a panel to house the label
      private JLabel label = new JLabel();       //a label to house the image
      private String[] image = {"Jack in the Box 1.png","Jack in the Box 2.png","Jack in the Box 3.png","Jack in the Box 4.png","Jack in the Box 5.png","Jack in the Box 6.png","Jack in the Box 7.png"}; //an array to hold the frames of the animation
      private ImageIcon[] icon = new ImageIcon[7]; //an array of icons to be the images
      private JFrame f;

private TextOverlay TO;

private Timer timer;
private Timer timer2;
int x = 0;
int y = 4;
int counter = 0;
/**
 * Constructor for objects of class Reveal
 */
public Reveal(String name, int number) 
{ 
    TO = new TextOverlay("Jack in the Box 7.png", name, number);

    for (int h = 0; h < 7; h++){
      icon[h] = new ImageIcon(image[h]);
      icon[h].getImage();
    }

    JFrame f = new JFrame();
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.setVisible(true);

    //Sets the size of the window
    f.setSize(800,850);
    panel = new JPanel();
    label = new JLabel();
    label.setIcon( icon[x] );
    panel.add(label);


    setVisible(true);

    f.add(panel);
    display(name, number);
    **f.add(TO);**

}

public void display(String name, int number){
    timer = new Timer(150, new ActionListener(){
        public void actionPerformed(ActionEvent e) {
            if (counter > 27){
            timer.stop();
            timer2.start(); //starts the second half of the animation
          }else{

            if (x != 3){
                x++;
            }else{
                x = 0;
            }
            label.setIcon( icon[x] );
            counter++;
          } //ends if-else
        } //ends action method
    }); //ends timer

    timer2 = new Timer(250, new ActionListener(){
        public void actionPerformed(ActionEvent e){ 
          if (y > 6) {   
            timer2.stop();
          }else{
            label.setIcon( icon[y] );
            y++;
          } //ends if-else
        } //ends action method
    }); //ends timer2

    timer.start();
    }

}
**

}

1 个答案:

答案 0 :(得分:0)

因此,您似乎对Swing的工作方式有误解,您可能会找到How to Use Swing TimersConcurrency in Swing的帮助。

基本上,当您startTimer时,它不会在此时阻塞,直到计时器结束为止(即使这样做了,您也无法按照您希望的方式工作) 。而是创建一个新线程,并在指定时间段后在事件调度线程上放置一个请求,以执行提供的Runnable

这意味着当您执行类似的操作...

f.add(panel);
display(name, number);
f.add(TO);

您实际上是在TO上添加JLabel组件(因为框架使用的是BorderLayout,并且CENTRE位置是默认位置。

相反,在您的第二个计时器完成时,您需要删除标签并添加TO组件...

timer2 = new Timer(250, new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        if (y > 6) {
            timer2.stop();
            Container parent = label.getParent();
            parent.remove(label);
            parent.add(TO);
            parent.revalidate();
        } else {
            label.setIcon(icon[y]);
            y++;
        } //ends if-else
    } //ends action method
}); //ends timer2

可运行示例...

import java.awt.event.*;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Font;
import java.awt.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.border.LineBorder;

public class Reveal extends JPanel {

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }
                new Reveal("Test", 5);
            }
        });
    }

    private JPanel panel = new JPanel();       //a panel to house the label
    private JLabel label = new JLabel();       //a label to house the image
    private ImageIcon[] icon = new ImageIcon[7]; //an array of icons to be the images
    private JFrame f;

    private TextOverlay TO;

    private Timer timer;
    private Timer timer2;
    int x = 0;
    int y = 4;
    int counter = 0;

    /**
     * Constructor for objects of class Reveal
     */
    public Reveal(String name, int number) {
        TO = new TextOverlay("Jack in the Box 7.png", name, number);

        for (int h = 0; h < 7; h++) {
            icon[h] = new ImageIcon(makeImage(h));
            icon[h].getImage();
        }

        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setVisible(true);

        //Sets the size of the window
        f.setSize(800, 850);
        panel = new JPanel(new GridBagLayout());
        label = new JLabel();
        label.setIcon(icon[x]);
        label.setBorder(new LineBorder(Color.RED));
        panel.add(label);

        f.add(panel);
        display(name, number);
//      f.add(TO);

        setVisible(true);
    }

    public void display(String name, int number) {
        timer = new Timer(150, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (counter > 27) {
                    timer.stop();
                    timer2.start(); //starts the second half of the animation
                } else {

                    if (x != 3) {
                        x++;
                    } else {
                        x = 0;
                    }
                    label.setIcon(icon[x]);
                    counter++;
                } //ends if-else
            } //ends action method
        }); //ends timer

        timer2 = new Timer(250, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (y > 6) {
                    timer2.stop();
                    Container parent = label.getParent();
                    parent.remove(label);
                    parent.add(TO);
                    parent.revalidate();
                } else {
                    label.setIcon(icon[y]);
                    y++;
                } //ends if-else
            } //ends action method
        }); //ends timer2

        timer.start();
    }

    protected BufferedImage makeImage(int h) {
        BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = img.createGraphics();
        FontMetrics fm = g2d.getFontMetrics();
        String text = Integer.toString(h);
        int x = (100 - fm.stringWidth(text)) / 2;
        int y = ((100 - fm.getHeight()) / 2) + fm.getAscent();
        g2d.setColor(Color.BLUE);
        g2d.fillRect(0, 0, 100, 100);
        g2d.setColor(Color.BLACK);
        g2d.drawString(text, x, y);
        g2d.dispose();
        return img;
    }

    public class TextOverlay extends JPanel {

        private BufferedImage image;
        private String name;
        private String fileX;
        private int number;

        public TextOverlay(String f, String s, int n) {
            name = s;
            number = n;
            fileX = f;

            image = makeImage(n);

            image = process(image, name, number);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.drawImage(image, 0, 0, this);
        }

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

        private BufferedImage process(BufferedImage old, String name, int number) {
            int w = old.getWidth();
            int h = old.getHeight();
            BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g2d = img.createGraphics();
            g2d.drawImage(old, 0, 0, w, h, this);
            g2d.setPaint(Color.black);
            g2d.setFont(new Font("Franklin Gothic Demi Cond", Font.PLAIN, 30));
            String s1 = name;
            String s2 = Integer.toString(number);;
            FontMetrics fm = g2d.getFontMetrics();
            g2d.drawString(s1, 40, 90);
            g2d.drawString(s2, 40, 140);
            g2d.dispose();
            return img;
        }
    }

}

“略有不同”的方法...

动画实际上是一个非常复杂的主题,很难实现。

这就是为什么当遇到诸如此类的问题时,我更喜欢看已经实现的库来帮助解决它们。我建议看看:

作为一些起点。

虽然我更喜欢使用库,但有时还是无法使用,或者库不能满足我的整体需求……而且我喜欢涉猎……这是一种爱好。

根据我从您的代码中可以了解的内容,您正在尝试从快速动画开始,然后将其放慢直到最后一帧。在动画理论中,这通常称为缓动,更具体地讲,是“慢/缓”。

以下内容是从我一直在玩的一小段片段(设计一个可重用的库)中借来的,这些片段将基本(随机)显示4秒钟内的图像,动画速度变慢,最后呈现“幸运”数字

Really bad screen grab

nb gif动画实际上真的很慢,您需要运行它才能看到不同之处

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.swing.*;

public class Reveal extends JPanel {

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

    public Reveal() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                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);
            }
        });
    }

    public class TestPane extends JPanel {

        private IntAnimatable animatable;

        private List<ImageIcon> icons = new ArrayList<>(25);
        private JLabel label = new JLabel();

        public TestPane() {
            setLayout(new GridBagLayout());
            IntRange range = new IntRange(0, 111);
            animatable = new IntAnimatable(range, Duration.ofSeconds(4), Easement.SLOWOUT, new AnimatableListener<Integer>() {
                @Override
                public void animationChanged(Animatable<Integer> animator) {
                    int value = animator.getValue();
                    int index = value % 7;
                    ImageIcon icon = icons.get(index);
                    if (label.getIcon() != icon) {
                        label.setIcon(icon);
                    }
                }
            }, new AnimatableLifeCycleAdapter<Integer>() {
                @Override
                public void animationCompleted(Animatable<Integer> animator) {
                    BufferedImage img = makeImage(3);
                    writeTextOverImage("Lucky number", img);
                    ImageIcon luckNumber = new ImageIcon(img);
                    label.setIcon(luckNumber);
                }
            });

            for (int index = 0; index < 7; index++) {
                icons.add(new ImageIcon(makeImage(index)));
            }
            Collections.shuffle(icons);

            add(label);

            Animator.INSTANCE.add(animatable);
        }

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

    }

    protected void writeTextOverImage(String text, BufferedImage img) {
        Graphics2D g2d = img.createGraphics();
        Font font = g2d.getFont();
        font = font.deriveFont(Font.BOLD, font.getSize2D() + 2);
        g2d.setFont(font);
        FontMetrics fm = g2d.getFontMetrics();
        int width = img.getWidth();
        int height = img.getWidth();
        int x = (width - fm.stringWidth(text)) / 2;
        int y = fm.getAscent();
        g2d.setColor(Color.YELLOW);
        g2d.drawString(text, x, y);
        g2d.dispose();
    }

    protected BufferedImage makeImage(int h) {
        BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = img.createGraphics();
        FontMetrics fm = g2d.getFontMetrics();
        String text = Integer.toString(h);
        int x = (100 - fm.stringWidth(text)) / 2;
        int y = ((100 - fm.getHeight()) / 2) + fm.getAscent();
        g2d.setColor(Color.BLUE);
        g2d.fillRect(0, 0, 100, 100);
        g2d.setColor(Color.WHITE);
        g2d.drawString(text, x, y);
        g2d.dispose();
        return img;
    }

    /**** Range ****/
    /* 
    A lot of animation is done from one point to another, this just
    provides a self contained concept of a range which can be used to 
    calculate the value based on the current progression over time
    */

    public abstract class Range<T> {

        private T from;
        private T to;

        public Range(T from, T to) {
            this.from = from;
            this.to = to;
        }

        public T getFrom() {
            return from;
        }

        public T getTo() {
            return to;
        }

        @Override
        public String toString() {
            return "From " + getFrom() + " to " + getTo();
        }

        public abstract T valueAt(double progress);

    }

    public class IntRange extends Range<Integer> {

        public IntRange(Integer from, Integer to) {
            super(from, to);
        }

        public Integer getDistance() {
            return getTo() - getFrom();
        }

        @Override
        public Integer valueAt(double progress) {
            int distance = getDistance();
            int value = (int) Math.round((double) distance * progress);
            value += getFrom();

            return value;
        }
    }

    /**** Animatable ****/
    /*
    The core concept of something that is animatable.  This basic wraps up the 
    logic for calculating the progression of the animation over a period of time
    and then use that to calculate the value of the range and then the observers
    are notified so they can do stuff
    */

    public class IntAnimatable extends AbstractAnimatableRange<Integer> {

        public IntAnimatable(IntRange animationRange, Duration duration, Easement easement, AnimatableListener<Integer> listener, AnimatableLifeCycleListener<Integer> lifeCycleListener) {
            super(animationRange, duration, easement, listener, lifeCycleListener);
        }

    }

    public interface AnimatableListener<T> {

        public void animationChanged(Animatable<T> animator);
    }

    public interface AnimatableLifeCycleListener<T> {

        public void animationStopped(Animatable<T> animator);

        public void animationCompleted(Animatable<T> animator);

        public void animationStarted(Animatable<T> animator);

        public void animationPaused(Animatable<T> animator);
    }

    public interface Animatable<T> {

        public T getValue();

        public void tick();

        public Duration getDuration();

        public Easement getEasement();

        // Wondering if these should be part of a secondary interface
        // Provide a "self managed" unit of work
        public void start();

        public void stop();

        public void pause();
    }

    public class AnimatableLifeCycleAdapter<T> implements AnimatableLifeCycleListener<T> {

        @Override
        public void animationStopped(Animatable<T> animator) {
        }

        @Override
        public void animationCompleted(Animatable<T> animator) {
        }

        @Override
        public void animationStarted(Animatable<T> animator) {
        }

        @Override
        public void animationPaused(Animatable<T> animator) {
        }

    }

    public abstract class AbstractAnimatable<T> implements Animatable<T> {

        private LocalDateTime startTime;
        private Duration duration = Duration.ofSeconds(5);
        private AnimatableListener<T> animatableListener;
        private AnimatableLifeCycleListener<T> lifeCycleListener;
        private Easement easement;
        private double rawOffset;

        public AbstractAnimatable(Duration duration, AnimatableListener<T> listener) {
            this.animatableListener = listener;
            this.duration = duration;
        }

        public AbstractAnimatable(Duration duration, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
            this(duration, listener);
            this.lifeCycleListener = lifeCycleListener;
        }

        public AbstractAnimatable(Duration duration, Easement easement, AnimatableListener<T> listener) {
            this(duration, listener);
            this.easement = easement;
        }

        public AbstractAnimatable(Duration duration, Easement easement, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
            this(duration, easement, listener);
            this.lifeCycleListener = lifeCycleListener;
        }

        public void setEasement(Easement easement) {
            this.easement = easement;
        }

        @Override
        public Easement getEasement() {
            return easement;
        }

        public Duration getDuration() {
            return duration;
        }

        protected void setDuration(Duration duration) {
            this.duration = duration;
        }

        public double getCurrentProgress(double rawProgress) {
            Easement easement = getEasement();
            double progress = Math.min(1.0, Math.max(0.0, getRawProgress()));
            if (easement != null) {
                progress = easement.interpolate(progress);
            }
            return Math.min(1.0, Math.max(0.0, progress));
        }

        public double getRawProgress() {
            if (startTime == null) {
                return 0.0;
            }
            Duration duration = getDuration();
            Duration runningTime = Duration.between(startTime, LocalDateTime.now());
            double progress = rawOffset + (runningTime.toMillis() / (double) duration.toMillis());

            return Math.min(1.0, Math.max(0.0, progress));
        }

        @Override
        public void tick() {
            if (startTime == null) {
                startTime = LocalDateTime.now();
                fireAnimationStarted();
            }
            double rawProgress = getRawProgress();
            double progress = getCurrentProgress(rawProgress);
            if (rawProgress >= 1.0) {
                progress = 1.0;
            }

            tick(progress);

            fireAnimationChanged();
            if (rawProgress >= 1.0) {
                fireAnimationCompleted();
            }
        }

        protected abstract void tick(double progress);

        @Override
        public void start() {
            if (startTime != null) {
                // Restart?
                return;
            }
            Animator.INSTANCE.add(this);
        }

        @Override
        public void stop() {
            stopWithNotitifcation(true);
        }

        @Override
        public void pause() {
            rawOffset += getRawProgress();
            stopWithNotitifcation(false);

            double remainingProgress = 1.0 - rawOffset;
            Duration remainingTime = getDuration().minusMillis((long) remainingProgress);
            setDuration(remainingTime);

            lifeCycleListener.animationStopped(this);
        }

        protected void fireAnimationChanged() {
            if (animatableListener == null) {
                return;
            }
            animatableListener.animationChanged(this);
        }

        protected void fireAnimationCompleted() {
            stopWithNotitifcation(false);
            if (lifeCycleListener == null) {
                return;
            }
            lifeCycleListener.animationCompleted(this);
        }

        protected void fireAnimationStarted() {
            if (lifeCycleListener == null) {
                return;
            }
            lifeCycleListener.animationStarted(this);
        }

        protected void fireAnimationPaused() {
            if (lifeCycleListener == null) {
                return;
            }
            lifeCycleListener.animationPaused(this);
        }

        protected void stopWithNotitifcation(boolean notify) {
            Animator.INSTANCE.remove(this);
            startTime = null;
            if (notify) {
                if (lifeCycleListener == null) {
                    return;
                }
                lifeCycleListener.animationStopped(this);
            }
        }

    }

    public abstract class AbstractAnimatableRange<T> extends AbstractAnimatable<T> {

        private Range<T> range;
        private T value;

        public AbstractAnimatableRange(Range<T> range, Duration duration, AnimatableListener<T> listener) {
            super(duration, listener);
            this.range = range;
        }

        public AbstractAnimatableRange(Range<T> range, Duration duration, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
            super(duration, listener, lifeCycleListener);
            this.range = range;
        }

        public AbstractAnimatableRange(Range<T> range, Duration duration, Easement easement, AnimatableListener<T> listener) {
            super(duration, easement, listener);
            this.range = range;
        }

        public AbstractAnimatableRange(Range<T> range, Duration duration, Easement easement, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
            super(duration, easement, listener, lifeCycleListener);
            this.range = range;
        }

        protected void tick(double progress) {
            setValue(range.valueAt(progress));
        }

        protected void setValue(T value) {
            this.value = value;
        }

        @Override
        public T getValue() {
            return value;
        }

    }

    /*
    Easement, complicated, but fun
    */
    public enum Easement {
        SLOWINSLOWOUT(1d, 0d, 0d, 1d), FASTINSLOWOUT(0d, 0d, 1d, 1d), SLOWINFASTOUT(0d, 1d, 0d, 0d), SLOWIN(1d, 0d, 1d, 1d), SLOWOUT(0d, 0d, 0d, 1d);
        private final double[] points;
        private final List<PointUnit> normalisedCurve;

        private Easement(double x1, double y1, double x2, double y2) {
            points = new double[]{x1, y1, x2, y2};
            final List<Double> baseLengths = new ArrayList<>();
            double prevX = 0;
            double prevY = 0;
            double cumulativeLength = 0;
            for (double t = 0; t <= 1; t += 0.01) {
                Point2D xy = getXY(t);
                double length = cumulativeLength + Math.sqrt((xy.getX() - prevX) * (xy.getX() - prevX) + (xy.getY() - prevY) * (xy.getY() - prevY));
                baseLengths.add(length);
                cumulativeLength = length;
                prevX = xy.getX();
                prevY = xy.getY();
            }
            normalisedCurve = new ArrayList<>(baseLengths.size());
            int index = 0;
            for (double t = 0; t <= 1; t += 0.01) {
                double length = baseLengths.get(index++);
                double normalLength = length / cumulativeLength;
                normalisedCurve.add(new PointUnit(t, normalLength));
            }
        }

        public double interpolate(double fraction) {
            int low = 1;
            int high = normalisedCurve.size() - 1;
            int mid = 0;
            while (low <= high) {
                mid = (low + high) / 2;
                if (fraction > normalisedCurve.get(mid).getPoint()) {
                    low = mid + 1;
                } else if (mid > 0 && fraction < normalisedCurve.get(mid - 1).getPoint()) {
                    high = mid - 1;
                } else {
                    break;
                }
            }
            /*
         * The answer lies between the "mid" item and its predecessor.
             */
            final PointUnit prevItem = normalisedCurve.get(mid - 1);
            final double prevFraction = prevItem.getPoint();
            final double prevT = prevItem.getDistance();
            final PointUnit item = normalisedCurve.get(mid);
            final double proportion = (fraction - prevFraction) / (item.getPoint() - prevFraction);
            final double interpolatedT = prevT + (proportion * (item.getDistance() - prevT));
            return getY(interpolatedT);
        }

        protected Point2D getXY(double t) {
            final double invT = 1 - t;
            final double b1 = 3 * t * invT * invT;
            final double b2 = 3 * t * t * invT;
            final double b3 = t * t * t;
            final Point2D xy = new Point2D.Double((b1 * points[0]) + (b2 * points[2]) + b3, (b1 * points[1]) + (b2 * points[3]) + b3);
            return xy;
        }

        protected double getY(double t) {
            final double invT = 1 - t;
            final double b1 = 3 * t * invT * invT;
            final double b2 = 3 * t * t * invT;
            final double b3 = t * t * t;
            return (b1 * points[2]) + (b2 * points[3]) + b3;
        }

        protected class PointUnit {

            private final double distance;
            private final double point;

            public PointUnit(double distance, double point) {
                this.distance = distance;
                this.point = point;
            }

            public double getDistance() {
                return distance;
            }

            public double getPoint() {
                return point;
            }
        }

    }

    /**** Core Animation Engine ****/

    public enum Animator {
        INSTANCE;
        private Timer timer;
        private List<Animatable> properies;

        private Animator() {
            properies = new ArrayList<>(5);
            timer = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    List<Animatable> copy = new ArrayList<>(properies);
                    Iterator<Animatable> it = copy.iterator();
                    while (it.hasNext()) {
                        Animatable ap = it.next();
                        ap.tick();
                    }
                    if (properies.isEmpty()) {
                        timer.stop();
                    }
                }
            });
        }

        public void add(Animatable ap) {
            properies.add(ap);
            timer.start();
        }

        protected void removeAll(List<Animatable> completed) {
            properies.removeAll(completed);
        }

        public void remove(Animatable ap) {
            properies.remove(ap);
            if (properies.isEmpty()) {
                timer.stop();
            }
        }

    }

}