因此,我正在构建一个音乐播放器应用程序,该应用程序可以播放拖放到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等)
我该怎么做呢?

最佳答案

注意事项:


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


理论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值,但是您可以同样地更改表示该音符的对象的大小,但这将是一个不同的实施,这里没有介绍。

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

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

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

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

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

可运行的示例...

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

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

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);
        }

    }

}


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

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

在此示例中,我主要使用Animatable作为主要渲染引擎。但是,您可以像使用事件驱动框架一样轻松地使用链接在一起的各个组件。

08-04 21:06