因此,我正在构建一个音乐播放器应用程序,该应用程序可以播放拖放到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:如果我这样做的话,我会把解决方案抽象到更高的层次
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
作为主要渲染引擎。但是,您可以像使用事件驱动框架一样轻松地使用链接在一起的各个组件。