如何更改actionPerformed()中的Swing Timer Delay

时间:2018-10-23 19:58:25

标签: java swing user-interface timer java-10

因此,我正在构建一个音乐播放器应用程序,该应用程序可播放被拖放到JLabel上的音符。当我按下播放按钮时,我希望每个音符都以与该音符相对应的延迟值突出显示。我为此使用了Swing计时器,但问题是,它只是以构造函数中指定的恒定延迟循环。

playButton.addActionListener(e -> {
        timerI = 0;
        System.out.println("Entered onAction");

        Timer t = new Timer(1000, e1 -> {
            if (timerI < 24) {
                NoteLabel thisNote = (NoteLabel)staff.getComponent(timerI);
                NoteIcon thisIcon = thisNote.getIcon();
                String noteName = thisIcon.getNoteName();
                thisNote.setIcon(noteMap.get(noteName + "S"));
                timerI++;
            }
        });
        t.start();
    });

一切正常,但是我想使计时器延迟成为动态。每个NoteIcon对象都有一个包含延迟值的属性,我希望计时器等待不同的时间,具体取决于在该循环中获取的NoteIcon。 (在第一个循环中等待1秒钟,然后是2、4、1等) 我该怎么做呢?

1 个答案:

答案 0 :(得分:3)

注意事项:

  • 动画并不简单。情况很复杂。它周围有许多重要的理论,旨在使动画看起来不错
  • 好的动画很难
  • 动画是随着时间变化的幻觉
  • 我要介绍的大部分内容都是基于库代码的,因此会有些复杂,但是是为重用和抽象而设计的

理论tl; dr

好吧,一些非常无聊的理论。但是首先,我不会谈论的事情-缓动或动画曲线。这些改变了给定时间段内动画的播放速度,使动画看起来更自然,但是我可以花整个答案谈论其他事情:/

您要做的第一件事是抽象您的概念。例如。动画通常是随时间变化的(某些动画在无限长的时间内是线性的,但让我们尝试将其保持在问题范围之内)。

因此,我们马上有了两个重要的概念。第一个是持续时间,第二个是持续时间从A点到B点的标准化进度。也就是说,持续时间的一半将是0.5。这很重要,因为它允许我们抽象化概念并使框架动态化。

动画太快了吗?更改持续时间,其他所有内容均保持不变。

时间线...

好吧,音乐是一个时间表。它具有定义的起点和终点(再次保持简单)以及沿该时间线“执行操作”的事件,与音乐时间线无关(即,每个音符可以在指定的持续时间内播放,与音乐时间线无关),将继续前进甚至结束)

首先,我们需要一个注释...

public class Note {
    private Duration duration;

    public Note(Duration duration) {
        this.duration = duration;
    }

    public Duration getDuration() {
        return duration;
    }
}

还有一个基于“事件”的时间轴,该时间轴描述了何时应在正常时间段内弹奏这些音符。

public static class EventTimeLine<T> {

    private Map<Double, KeyFrame<T>> mapEvents;

    public EventTimeLine() {
        mapEvents = new TreeMap<>();
    }

    public void add(double progress, T value) {
        mapEvents.put(progress, new KeyFrame<T>(progress, value));
    }

    public List<T> getValues() {
        return Collections.unmodifiableList(mapEvents.values().stream()
                .map(kf -> kf.getValue())
                .collect(Collectors.toList()));
    }

    public double getPointOnTimeLineFor(T value) {
        for (Map.Entry<Double, KeyFrame<T>> entry : mapEvents.entrySet()) {
            if (entry.getValue().getValue() == value) {
                return entry.getKey();
            }
        }

        return -1;
    }

    public List<T> getValuesAt(double progress) {

        if (progress < 0) {
            progress = 0;
        } else if (progress > 1) {
            progress = 1;
        }

        return getKeyFramesBetween(progress, 0.01f)
                .stream()
                .map(kf -> kf.getValue())
                .collect(Collectors.toList());
    }

    public List<KeyFrame<T>> getKeyFramesBetween(double progress, double delta) {

        int startAt = 0;

        List<Double> keyFrames = new ArrayList<>(mapEvents.keySet());
        while (startAt < keyFrames.size() && keyFrames.get(startAt) <= progress - delta) {
            startAt++;
        }

        startAt = Math.min(keyFrames.size() - 1, startAt);
        int endAt = startAt;
        while (endAt < keyFrames.size() && keyFrames.get(endAt) <= progress + delta) {
            endAt++;
        }
        endAt = Math.min(keyFrames.size() - 1, endAt);

        List<KeyFrame<T>> frames = new ArrayList<>(5);
        for (int index = startAt; index <= endAt; index++) {
            KeyFrame<T> keyFrame = mapEvents.get(keyFrames.get(index));
            if (keyFrame.getProgress() >= progress - delta
                    && keyFrame.getProgress() <= progress + delta) {
                frames.add(keyFrame);
            }
        }

        return frames;

    }

    public class KeyFrame<T> {

        private double progress;
        private T value;

        public KeyFrame(double progress, T value) {
            this.progress = progress;
            this.value = value;
        }

        public double getProgress() {
            return progress;
        }

        public T getValue() {
            return value;
        }

        @Override
        public String toString() {
            return "KeyFrame progress = " + getProgress() + "; value = " + getValue();
        }

    }

}

然后您可以创建类似...的音乐时间轴

musicTimeLine = new EventTimeLine<Note>();
musicTimeLine.add(0.1f, new Note(Duration.ofMillis(1000)));
musicTimeLine.add(0.12f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.2f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.21f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.22f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.25f, new Note(Duration.ofMillis(1000)));
musicTimeLine.add(0.4f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.5f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.7f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.8f, new Note(Duration.ofMillis(2000)));

注意,这里我将注释定义为以固定的时间运行。您“可以”让它们按照时间轴的持续时间进行播放...但是只是说很难,所以我让您自己决定;)

动画引擎

提出的(简单的)动画引擎使用一个高速运行的Timer作为中央“滴答”引擎。

然后通知Animatable个实际执行基础动画的对象。

通常,我会在一系列值(从-到)之间设置动画,但是在这种情况下,我们实际上只对动画播放的时间感兴趣。由此,我们可以确定应该播放哪些音符并为该音符设置动画,在本示例中,请更改alpha值,但是您可以同样地更改表示音符的对象的大小,但这将是不同的{{ 1}}的实现,这里没有介绍。

如果您感兴趣,我的SuperSimpleSwingAnimationFramework(此示例大致基于此)包含基于“范围”的Animatable……有趣的东西。

在该示例中,使用Animatable来驱动音乐Animatable,该音乐仅检查时间轴上是否有需要在特定时间点播放的“音符”。

第二个EventTimeLine用于控制alpha值(0-1-0)。然后为每个便笺提供了自己的BlendingTimeLine,它驱动了此混合时间轴,并使用其值来动画显示突出显示的便笺的alpha。

这是API分离性质的一个很好的例子-Animatable用于所有注释。 BlendingTimeLine只需花费他们玩过的时间,然后从时间轴中提取所需的值并将其应用。

这意味着每个音符仅在其持续时间指定的情况下才突出显示,并且全部独立显示。

可运行示例...

nb:如果我这样做的话,我会把解决方案抽象到更高的水平

Twang my strings

Animatable

它“似乎”很复杂,它“似乎”很困难。但是,当您完成几次此类操作后,它将变得更加简单,并且解决方案变得更加有意义。

已解耦。可重复使用。它很灵活。

在此示例中,我主要使用import java.awt.AlphaComposite; 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.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.stream.Collectors; 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 TestPane extends JPanel { private EventTimeLine<Note> musicTimeLine; private DefaultDurationAnimatable timeLineAnimatable; private Double playProgress; private Set<Note> playing = new HashSet<Note>(5); private Map<Note, Double> noteAlpha = new HashMap<>(5); private DoubleBlender blender = new DoubleBlender(); private BlendingTimeLine<Double> alphaTimeLine = new BlendingTimeLine<>(blender); public TestPane() { musicTimeLine = new EventTimeLine<Note>(); musicTimeLine.add(0.1f, new Note(Duration.ofMillis(1000))); musicTimeLine.add(0.12f, new Note(Duration.ofMillis(500))); musicTimeLine.add(0.2f, new Note(Duration.ofMillis(500))); musicTimeLine.add(0.21f, new Note(Duration.ofMillis(500))); musicTimeLine.add(0.22f, new Note(Duration.ofMillis(500))); musicTimeLine.add(0.25f, new Note(Duration.ofMillis(1000))); musicTimeLine.add(0.4f, new Note(Duration.ofMillis(2000))); musicTimeLine.add(0.5f, new Note(Duration.ofMillis(2000))); musicTimeLine.add(0.7f, new Note(Duration.ofMillis(2000))); musicTimeLine.add(0.8f, new Note(Duration.ofMillis(2000))); alphaTimeLine.add(0.0f, 0.0); alphaTimeLine.add(0.5f, 1.0); alphaTimeLine.add(1.0f, 0.0); timeLineAnimatable = new DefaultDurationAnimatable(Duration.ofSeconds(10), new AnimatableListener() { @Override public void animationChanged(Animatable animator) { double progress = timeLineAnimatable.getPlayedDuration(); playProgress = progress; List<Note> notes = musicTimeLine.getValuesAt(progress); if (notes.size() > 0) { System.out.println(">> " + progress + " @ " + notes.size()); for (Note note : notes) { playNote(note); } } repaint(); } }, null); timeLineAnimatable.start(); } protected void playNote(Note note) { // Note is already playing... // Equally, we could maintain a reference to the animator, mapped to // the note, but what ever... if (playing.contains(note)) { return; } playing.add(note); DurationAnimatable noteAnimatable = new DefaultDurationAnimatable(note.getDuration(), new AnimatableListener() { @Override public void animationChanged(Animatable animator) { DurationAnimatable da = (DurationAnimatable) animator; double progress = da.getPlayedDuration(); double alpha = alphaTimeLine.getValueAt((float) progress); noteAlpha.put(note, alpha); repaint(); } }, new AnimatableLifeCycleListenerAdapter() { @Override public void animationCompleted(Animatable animator) { playing.remove(note); noteAlpha.remove(note); repaint(); } }); noteAnimatable.start(); } @Override public Dimension getPreferredSize() { return new Dimension(200, 100); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g.create(); int startX = 10; int endX = getWidth() - 10; int range = endX - startX; int yPos = getHeight() / 2; g2d.setColor(Color.DARK_GRAY); g2d.drawLine(startX, yPos, endX, yPos); List<Note> notes = musicTimeLine.getValues(); for (Note note : notes) { double potl = musicTimeLine.getPointOnTimeLineFor(note); double xPos = startX + (range * potl); // Technically, this could be cached... Ellipse2D notePoint = new Ellipse2D.Double(xPos - 2.5, yPos - 2.5, 5, 5); g2d.fill(notePoint); if (noteAlpha.containsKey(note)) { double alpha = noteAlpha.get(note); // I'm lazy :/ // It's just simpler to copy the current context, modify the // composite, paint and then dispose of, then trying to // track and reset the composite manually Graphics2D alpha2d = (Graphics2D) g2d.create(); alpha2d.setComposite(AlphaComposite.SrcOver.derive((float) alpha)); Ellipse2D playedNote = new Ellipse2D.Double(xPos - 5, yPos - 5, 10, 10); alpha2d.setColor(Color.RED); alpha2d.fill(playedNote); alpha2d.dispose(); } } double playXPos = startX + (range * playProgress); g2d.setColor(Color.RED); Line2D playLine = new Line2D.Double(playXPos, 0, playXPos, getHeight()); g2d.draw(playLine); g2d.dispose(); } } public class Note { private Duration duration; public Note(Duration duration) { this.duration = duration; } public Duration getDuration() { return duration; } } public static class EventTimeLine<T> { private Map<Double, KeyFrame<T>> mapEvents; public EventTimeLine() { mapEvents = new TreeMap<>(); } public void add(double progress, T value) { mapEvents.put(progress, new KeyFrame<T>(progress, value)); } public List<T> getValues() { return Collections.unmodifiableList(mapEvents.values().stream() .map(kf -> kf.getValue()) .collect(Collectors.toList())); } public double getPointOnTimeLineFor(T value) { for (Map.Entry<Double, KeyFrame<T>> entry : mapEvents.entrySet()) { if (entry.getValue().getValue() == value) { return entry.getKey(); } } return -1; } public List<T> getValuesAt(double progress) { if (progress < 0) { progress = 0; } else if (progress > 1) { progress = 1; } return getKeyFramesBetween(progress, 0.01f) .stream() .map(kf -> kf.getValue()) .collect(Collectors.toList()); } public List<KeyFrame<T>> getKeyFramesBetween(double progress, double delta) { int startAt = 0; List<Double> keyFrames = new ArrayList<>(mapEvents.keySet()); while (startAt < keyFrames.size() && keyFrames.get(startAt) <= progress - delta) { startAt++; } startAt = Math.min(keyFrames.size() - 1, startAt); int endAt = startAt; while (endAt < keyFrames.size() && keyFrames.get(endAt) <= progress + delta) { endAt++; } endAt = Math.min(keyFrames.size() - 1, endAt); List<KeyFrame<T>> frames = new ArrayList<>(5); for (int index = startAt; index <= endAt; index++) { KeyFrame<T> keyFrame = mapEvents.get(keyFrames.get(index)); if (keyFrame.getProgress() >= progress - delta && keyFrame.getProgress() <= progress + delta) { frames.add(keyFrame); } } return frames; } public class KeyFrame<T> { private double progress; private T value; public KeyFrame(double progress, T value) { this.progress = progress; this.value = value; } public double getProgress() { return progress; } public T getValue() { return value; } @Override public String toString() { return "KeyFrame progress = " + getProgress() + "; value = " + getValue(); } } } public static class BlendingTimeLine<T> { private Map<Float, KeyFrame<T>> mapEvents; private Blender<T> blender; public BlendingTimeLine(Blender<T> blender) { mapEvents = new TreeMap<>(); this.blender = blender; } public void setBlender(Blender<T> blender) { this.blender = blender; } public Blender<T> getBlender() { return blender; } public void add(float progress, T value) { mapEvents.put(progress, new KeyFrame<T>(progress, value)); } public T getValueAt(float progress) { if (progress < 0) { progress = 0; } else if (progress > 1) { progress = 1; } List<KeyFrame<T>> keyFrames = getKeyFramesBetween(progress); float max = keyFrames.get(1).progress - keyFrames.get(0).progress; float value = progress - keyFrames.get(0).progress; float weight = value / max; T blend = blend(keyFrames.get(0).getValue(), keyFrames.get(1).getValue(), 1f - weight); return blend; } public List<KeyFrame<T>> getKeyFramesBetween(float progress) { List<KeyFrame<T>> frames = new ArrayList<>(2); int startAt = 0; Float[] keyFrames = mapEvents.keySet().toArray(new Float[mapEvents.size()]); while (startAt < keyFrames.length && keyFrames[startAt] <= progress) { startAt++; } startAt = Math.min(startAt, keyFrames.length - 1); frames.add(mapEvents.get(keyFrames[startAt - 1])); frames.add(mapEvents.get(keyFrames[startAt])); return frames; } protected T blend(T start, T end, float ratio) { return blender.blend(start, end, ratio); } public static interface Blender<T> { public T blend(T start, T end, float ratio); } public class KeyFrame<T> { private float progress; private T value; public KeyFrame(float progress, T value) { this.progress = progress; this.value = value; } public float getProgress() { return progress; } public T getValue() { return value; } @Override public String toString() { return "KeyFrame progress = " + getProgress() + "; value = " + getValue(); } } } public class DoubleBlender implements BlendingTimeLine.Blender<Double> { @Override public Double blend(Double start, Double end, float ratio) { double ir = (double) 1.0 - ratio; return (double) (start * ratio + end * ir); } } 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(); } } } // Reprepresents a linear animation public interface Animatable { public void tick(); public void start(); public void stop(); } public interface DurationAnimatable extends Animatable { public Duration getDuration(); public Double getPlayedDuration(); } public abstract class AbstractAnimatable implements Animatable { private AnimatableListener animatableListener; private AnimatableLifeCycleListener lifeCycleListener; public AbstractAnimatable(AnimatableListener listener) { this(listener, null); } public AbstractAnimatable(AnimatableListener listener, AnimatableLifeCycleListener lifeCycleListener) { this.animatableListener = listener; this.lifeCycleListener = lifeCycleListener; } public AnimatableLifeCycleListener getLifeCycleListener() { return lifeCycleListener; } public AnimatableListener getAnimatableListener() { return animatableListener; } @Override public void tick() { fireAnimationChanged(); } @Override public void start() { fireAnimationStarted(); Animator.INSTANCE.add(this); } @Override public void stop() { fireAnimationStopped(); Animator.INSTANCE.remove(this); } protected void fireAnimationChanged() { if (animatableListener == null) { return; } animatableListener.animationChanged(this); } protected void fireAnimationStarted() { if (lifeCycleListener == null) { return; } lifeCycleListener.animationStarted(this); } protected void fireAnimationStopped() { if (lifeCycleListener == null) { return; } lifeCycleListener.animationStopped(this); } } public interface AnimatableListener { public void animationChanged(Animatable animator); } public interface AnimatableLifeCycleListener { public void animationCompleted(Animatable animator); public void animationStarted(Animatable animator); public void animationPaused(Animatable animator); public void animationStopped(Animatable animator); } public class AnimatableLifeCycleListenerAdapter implements AnimatableLifeCycleListener { @Override public void animationCompleted(Animatable animator) { } @Override public void animationStarted(Animatable animator) { } @Override public void animationPaused(Animatable animator) { } @Override public void animationStopped(Animatable animator) { } } public class DefaultDurationAnimatable extends AbstractAnimatable implements DurationAnimatable { private Duration duration; private Instant startTime; public DefaultDurationAnimatable(Duration duration, AnimatableListener listener, AnimatableLifeCycleListener lifeCycleListener) { super(listener, lifeCycleListener); this.duration = duration; } @Override public Duration getDuration() { return duration; } @Override public Double getPlayedDuration() { if (startTime == null) { return 0.0; } Duration duration = getDuration(); Duration runningTime = Duration.between(startTime, Instant.now()); double progress = (runningTime.toMillis() / (double) duration.toMillis()); return Math.min(1.0, Math.max(0.0, progress)); } @Override public void tick() { if (startTime == null) { startTime = Instant.now(); fireAnimationStarted(); } fireAnimationChanged(); if (getPlayedDuration() >= 1.0) { fireAnimationCompleted(); stop(); } } protected void fireAnimationCompleted() { AnimatableLifeCycleListener lifeCycleListener = getLifeCycleListener(); if (lifeCycleListener == null) { return; } lifeCycleListener.animationCompleted(this); } } } 作为主要渲染引擎。但是,您可以轻松地使用与某种事件驱动框架链接在一起的各个组件。