如何更改actionPerformed()中的Swing Timer Delay [英] How do I change Swing Timer Delay inside actionPerformed()

查看:66
本文介绍了如何更改actionPerformed()中的Swing Timer Delay的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

  playButton.addActionListener(e-> {timerI = 0;System.out.println("Entered onAction");计时器t =新计时器(1000,e1-> {如果(timerI< 24){NoteLabel thisNote =(NoteLabel)staff.getComponent(timerI);NoteIcon thisIcon = thisNote.getIcon();字符串noteName = thisIcon.getNoteName();thisNote.setIcon(noteMap.get(noteName +"S"));timerI ++;}});t.start();}); 

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

解决方案

注意事项:

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

理论tl; dr

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

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

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

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

时间线...

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

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

 公共类注意{私人持续时间;公共说明(持续时间){this.duration =持续时间;}公共持续时间getDuration(){返回持续时间;}} 

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

 公共静态类EventTimeLine< T>{私有Map< Double,KeyFrame< T>mapEvents;公共EventTimeLine(){mapEvents = new TreeMap<>();}公共无效添加(两倍进度,T值){mapEvents.put(进度,新的KeyFrame< T>(进度,值));}公共列表< T>getValues(){返回Collections.unmodifiableList(mapEvents.values().stream().map(kf-> kf.getValue()).collect(Collectors.toList()));}public double getPointOnTimeLineFor(T value){用于(Map.Entry< Double,KeyFrame< T>>)条目:mapEvents.entrySet()){如果(entry.getValue().getValue()==值){返回entry.getKey();}}返回-1;}公共列表< T>getValuesAt(双倍进度){如果(进度< 0){进度= 0;}否则,如果(进度> 1){进度= 1;}返回getKeyFramesBetween(progress,0.01f).溪流().map(kf-> kf.getValue()).collect(Collectors.toList());}公共列表< KeyFrame< T>>getKeyFramesBetween(双进度,双增量){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)< =进度+增量){endAt ++;}endAt = Math.min(keyFrames.size()-1,endAt);列表< KeyFrame< T>>帧=新的ArrayList(5);for(int index = startAt; index< = endAt; index ++){关键帧TkeyFrame = mapEvents.get(keyFrames.get(index));如果(keyFrame.getProgress()> =进度-增量&&keyFrame.getProgress()< =进度+增量){frame.add(keyFrame);}}返回帧;}公共类KeyFrame< T>{私人双重进步;私人T值;公共KeyFrame(双进度,T值){this.progress =进度;this.value =值;}公共双重getProgress(){返回进度;}公共T getValue(){返回值;}@Override公共字符串toString(){返回"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,新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值,但是您可以同样地更改表示该音符的对象的大小,但这将是一个不同的 Animable 实现,这里没有介绍.

如果您感兴趣,我的

  import java.awt.AlphaComposite;导入java.awt.Color;导入java.awt.Dimension;导入java.awt.EventQueue;导入java.awt.Graphics;导入java.awt.Graphics2D;导入java.awt.event.ActionEvent;导入java.awt.event.ActionListener;导入java.awt.geom.Ellipse2D;导入java.awt.geom.Line2D;导入java.time.Duration;导入java.time.Instant;导入java.util.ArrayList;导入java.util.Collections;导入java.util.HashMap;导入java.util.HashSet;导入java.util.Iterator;导入java.util.List;导入java.util.Map;导入java.util.Set;导入java.util.TreeMap;导入java.util.stream.Collectors;导入javax.swing.JFrame;导入javax.swing.JPanel;导入javax.swing.Timer;公开课测试{公共静态void main(String [] args){新的Test();}公共Test(){EventQueue.invokeLater(new Runnable(){@Override公共无效run(){JFrame框架=新的JFrame();frame.add(new TestPane());frame.pack();frame.setLocationRelativeTo(null);frame.setVisible(true);}});}公共类TestPane扩展了JPanel {私人EventTimeLine< Note>musicTimeLine;私人DefaultDurationAnimatable timeLineAnimatable;私人Double playProgress;私人套装< Note>播放=新的HashSet< Note>(5);私人地图<注意,Double>noteAlpha =新的HashMap<>(5);私人DoubleBlender搅拌器= new DoubleBlender();私人BlendingTimeLine< Double>alphaTimeLine =新的BlendingTimeLine<>(blender);公共TestPane(){musicTimeLine =新的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,新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),新的AnimatableListener(){@Overridepublic void animationChanged(Animateable animator){双进度= timeLineAnimatable.getPlayedDuration();playProgress =进度;列表<注意>notes = musicTimeLine.getValuesAt(progress);如果(notes.size()> 0){System.out.println(>>" +进度+"@" + notes.size());对于(注意":注意"){playNote(note);}}repaint();}}, 空值);timeLineAnimatable.start();}受保护的无效playNote(注意事项){//注意已经在播放中...//同样,我们可以维护对动画师的引用,将其映射到//笔记,但无论如何...if(playing.contains(note)){返回;}playing.add(note);DurationAnimatable noteAnimatable =新的DefaultDurationAnimatable(note.getDuration(),新的AnimatableListener(){@Overridepublic void animationChanged(Animateable animator){DurationAnimatable da =(DurationAnimatable)动画师;双进度= da.getPlayedDuration();double alpha = alphaTimeLine.getValueAt((float)progress);noteAlpha.put(note,alpha);repaint();}},新的AnimatableLifeCycleListenerAdapter(){@Overridepublic void animationCompleted(Animateable animator){play.remove(note);noteAlpha.remove(note);repaint();}});noteAnimatable.start();}@Override公共维度getPreferredSize(){返回新的Dimension(200,100);}@Override受保护的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);列表< Note>notes = musicTimeLine.getValues();对于(注意":注意"){double potl = musicTimeLine.getPointOnTimeLineFor(note);double xPos = startX +(范围* potl);//从技术上讲,可以将其缓存...Ellipse2D notePoint =新的Ellipse2D.Double(xPos-2.5,yPos-2.5,5,5);g2d.fill(notePoint);如果(noteAlpha.containsKey(note)){double alpha = noteAlpha.get(note);//我很懒 :///复制当前上下文,修改//复合材料,先上漆,然后处置,然后尝试//手动跟踪和重置合成Graphics2D alpha2d =(Graphics2D)g2d.create();alpha2d.setComposite(AlphaComposite.SrcOver.derive((float)alpha));Ellipse2D播放注=新的Ellipse2D.Double(xPos-5,yPos-5,10,10);alpha2d.setColor(Color.RED);alpha2d.fill(playedNote);alpha2d.dispose();}}double playXPos = startX +(范围* playProgress);g2d.setColor(Color.RED);Line2D playLine =新的Line2D.Double(playXPos,0,playXPos,getHeight());g2d.draw(playLine);g2d.dispose();}}公开课注意事项{私人持续时间;公共说明(持续时间){this.duration =持续时间;}公共持续时间getDuration(){返回持续时间;}}公共静态类EventTimeLine< T>{私有Map< Double,KeyFrame< T>mapEvents;公共EventTimeLine(){mapEvents = new TreeMap<>();}公共无效添加(两倍进度,T值){mapEvents.put(进度,新的KeyFrame< T>(进度,值));}公共列表< T>getValues(){返回Collections.unmodifiableList(mapEvents.values().stream().map(kf-> kf.getValue()).collect(Collectors.toList()));}public double getPointOnTimeLineFor(T value){用于(Map.Entry< Double,KeyFrame< T>>)条目:mapEvents.entrySet()){如果(entry.getValue().getValue()==值){返回entry.getKey();}}返回-1;}公共列表< T>getValuesAt(双倍进度){如果(进度< 0){进度= 0;}否则,如果(进度> 1){进度= 1;}返回getKeyFramesBetween(progress,0.01f).溪流().map(kf-> kf.getValue()).collect(Collectors.toList());}公共列表< KeyFrame< T>>getKeyFramesBetween(双进度,双增量){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)< =进度+增量){endAt ++;}endAt = Math.min(keyFrames.size()-1,endAt);列表< KeyFrame< T>>帧=新的ArrayList(5);for(int index = startAt; index< = endAt; index ++){关键帧TkeyFrame = mapEvents.get(keyFrames.get(index));如果(keyFrame.getProgress()> =进度-增量&&keyFrame.getProgress()< =进度+增量){frame.add(keyFrame);}}返回帧;}公共类KeyFrame< T>{私人双重进步;私人T值;公共KeyFrame(双进度,T值){this.progress =进度;this.value =值;}公共双重getProgress(){返回进度;}公共T getValue(){返回值;}@Override公共字符串toString(){返回"KeyFrame progress =" + getProgress()+; value =" + getValue();}}}公共静态类BlendingTimeLine< T>{私有Map< Float,KeyFrame< T>mapEvents;私人Blender< T>搅拌机;公共BlendingTimeLine(Blender< T>搅拌器){mapEvents = new TreeMap<>();this.blender =搅拌机;}公共无效setBlender(Blender< T>搅拌器){this.blender =搅拌机;}公共搅拌器getBlender(){返回搅拌机;}public void add(浮动进度,T值){mapEvents.put(进度,新的KeyFrame< T>(进度,值));}公众T getValueAt(浮动进度){如果(进度< 0){进度= 0;}否则,如果(进度> 1){进度= 1;}列表< KeyFrame< T>>keyFrames = getKeyFramesBetween(进度);float max = keyFrames.get(1).progress-keyFrames.get(0).progress;浮点值=进度-keyFrames.get(0).progress;浮重=值/最大值;T blend = blend(keyFrames.get(0).getValue(),keyFrames.get(1).getValue(),1f-权重);返回混合;}公共列表< KeyFrame< T>>getKeyFramesBetween(浮动进度){列表< KeyFrame< T>>帧=新的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);frame.add(mapEvents.get(keyFrames [startAt-1]));frame.add(mapEvents.get(keyFrames [startAt]));返回帧;}受保护的T混合(T开始,T结束,浮动比率){return blender.blend(开始,结束,比率);}公共静态接口Blender< T>{公共T混合(T开始,T结束,浮动比率);}公共类KeyFrame< T>{私人浮动进度;私人T值;public KeyFrame(浮动进度,T值){this.progress =进度;this.value =值;}公众持股getProgress(){返回进度;}公共T getValue(){返回值;}@Override公共字符串toString(){返回"KeyFrame progress =" + getProgress()+; value =" + getValue();}}}公共类DoubleBlender实现BlendingTimeLine.Blender< Double>{@Override公共双重混合(双重开始,双重结束,浮动比率){double ir =(double)1.0-ratio;return(double)(开始*比率+结束* ir);}}公共枚举Animator {实例;私人计时器计时器;私有列表< Animatable>特质;私人Animator(){属性=新的ArrayList(5);timer = new Timer(5,new ActionListener(){@Override公共无效actionPerformed(ActionEvent e){List< Animatable>复制=新的ArrayList<>(属性);Iterator< Animatable>它= copy.iterator();而(it.hasNext()){可动画的ap = it.next();ap.tick();}如果(properies.isEmpty()){timer.stop();}}});}public void add(Animatable ap){属性.add(ap);timer.start();}受保护的void removeAll(List< Animatable>已完成){properties.removeAll(已完成);}public void remove(Animateable ap){属性.remove(ap);如果(properies.isEmpty()){timer.stop();}}}//表示线性动画公共接口动画{公共无效tick();public void start();公共无效stop();}公共接口DurationAnimatable扩展了Animatable {公共持续时间getDuration();公共Double getPlayedDuration();}公共抽象类AbstractAnimatable实现了Animatable {私有AnimatableListener animatableListener;私有AnimatableLifeCycleListener lifeCycleListener;公共AbstractAnimatable(AnimatableListener监听器){this(listener,null);}public AbstractAnimatable(AnimatableListener listener,AnimatableLifeCycleListener lifeCycleListener){this.animatableListener =侦听器;this.lifeCycleListener = lifeCycleListener;}公共AnimatableLifeCycleListener getLifeCycleListener(){返回lifeCycleListener;}公共AnimatableListener getAnimatableListener(){返回animatableListener;}@Override公共无效tick(){fireAnimationChanged();}@Overridepublic void start(){fireAnimationStarted();Animator.INSTANCE.add(this);}@Override公共无效stop(){fireAnimationStopped();Animator.INSTANCE.remove(this);}受保护的void fireAnimationChanged(){如果(animatableListener == null){返回;}animatableListener.animationChanged(this);}受保护的void fireAnimationStarted(){如果(lifeCycleListener == null){返回;}lifeCycleListener.animationStarted(this);}受保护的void fireAnimationStopped(){如果(lifeCycleListener == null){返回;}lifeCycleListener.animationStopped(this);}}公共接口AnimatableListener {public void animationChanged(Animateable animator);}公共接口AnimatableLifeCycleListener {public void animationCompleted(Animateable animator);public void animationStarted(Animateable animator);public void animationPaused(Animateable animator);public void animationStopped(Animateable animator);}公共类AnimatableLifeCycleListenerAdapter实现AnimatableLifeCycleListener {@Overridepublic void animationCompleted(Animateable animator){}@Overridepublic void animationStarted(动画师){}@Overridepublic void animationPaused(Animateable animator){}@Overridepublic void animationStopped(Animateable animator){}}公共类DefaultDurationAnimatable扩展AbstractAnimatable实现DurationAnimatable {私人持续时间;私有即时startTime;public DefaultDurationAnimatable(Duration duration,AnimatableListener listener,AnimatableLifeCycleListener lifeCycleListener){超级(侦听器,lifeCycleListener);this.duration =持续时间;}@Override公共持续时间getDuration(){返回持续时间;}@Override公开Double getPlayedDuration(){如果(startTime == null){返回0.0;}持续时间持续时间= getDuration();持续时间runningTime = Duration.between(startTime,Instant.now());双倍进度=(runningTime.toMillis()/(double)duration.toMillis());返回Math.min(1.0,Math.max(0.0,progress));}@Override公共无效tick(){如果(startTime == null){startTime = Instant.now();fireAnimationStarted();}fireAnimationChanged();如果(getPlayedDuration()> = 1.0){fireAnimationCompleted();停止();}}受保护的void fireAnimationCompleted(){AnimatableLifeCycleListener lifeCycleListener = getLifeCycleListener();如果(lifeCycleListener == null){返回;}lifeCycleListener.animationCompleted(this);}}} 

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

已解耦.它是可重用的.它很灵活.

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

So I'm building this music player app which plays notes which are dragged and dropped onto a JLabel. When I hit the play button, I want each note to be highlighted with a delay value corresponding to that note. I used a Swing Timer for this but the problem is, it just loops with a constant delay which is specified in the constructor.

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

It works and all, but I want to make the timer delay dynamic. Each NoteIcon object has an attribute which holds a delay value and I want the timer to wait a different amount of time depending on which NoteIcon is fetched in that loop. (For exait 1 sec for the first loop, then 2, 4, 1 etc) How do I do this?

解决方案

Caveats:

  • Animation is NOT simple. It's complicated. It has a number of important theories around it which are designed to make animation look good
  • Good animation is hard
  • Animation is the illusion of change over time
  • Much of what I'm presenting is based on library code, so it will be slightly convoluted, but is designed for re-use and abstraction

Theory tl;dr

Okay, some really boring theory. But first, things I'm not going to talk about - easement or animation curves. These change the speed at which animation is played over a given period of time, making the animation look more natural, but I could spend the entire answer talking about nothing else :/

The first thing you want to do is abstract your concepts. For example. Animation is typically a change over time (some animation is linear over an infinite amount of time, but, let's try and keep it within the confines of the question).

So immediately, we have two important concepts. The first is duration, the second is the normalised progress from point A to point B over that duration. That is, at half the duration, the progression will be 0.5. This is important, as it allows us to abstract the concepts and make the framework dynamic.

Animation too fast? Change the duration and everything else remains unchanged.

A timeline...

Okay, music is a timeline. It has a defined start and end point (again, keep it simple) and events along that timeline which "do stuff", independent of the music timeline (ie, each note can play for a specified duration, independent of the music timeline, which will have moved on or even finished)

First, we need a note...

public class Note {
    private Duration duration;

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

    public Duration getDuration() {
        return duration;
    }
}

And a "event" based timeline, which describes when those notes should be played over a normalised period of time.

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

    }

}

Then you could create a music timeline something like...

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

Note, here I've defined the notes as running at a fixed duration. You "could" make them play as a percentage of the duration of the timeline ... but just saying that is hard, so I'll leave that up to you ;)

Animation Engine

The presented (simple) animation engine, uses a single Timer, running at high speed, as a central "tick" engine.

It then notifies Animatable objects which actually perform the underlying animation.

Normally, I animated over a range of values (from - to), but in this case, we're actually only interested in the amount of time that the animation has played. From that we can determine what notes should be getting played AND animate the notes, in the case of this example, change the alpha value, but you could equally change the size of the objects representing the notes, but that would be a different Animatable implementation, which I've not presented here.

If you're interested, my SuperSimpleSwingAnimationFramework, which this example is loosely based on, contains "range" based Animatables ... fun stuff.

In the example, an Animatable is used to drive the music EventTimeLine, which simply checks the timeline for any "notes" which need to be played at the specific point in time.

A second BlendingTimeLine is used to control the alpha value (0-1-0). Each note is then provided with it's own Animatable which drives this blending timeline, and uses its values to animate the change in the alpha of the highlighted note.

This is a great example of the decoupled nature of the API - the BlendingTimeLine is used for ALL the notes. The Animatables simply take the amount of time they have played and extract the required value from the timeline and apply it.

This means that each note is only highlighted as long as its own duration specifies, all independently.

Runnable Example...

nb: If I was doing this, I'd have abstracted the solution to a much higher level

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

    }

}

Yes it "seems" complicated, yes it "seems" hard. But when you've done this kind of thing a few times, it becomes simpler and the solution makes a lot more sense.

It's decoupled. It's re-usable. It's flexible.

In this example, I've mostly used paintComponent as the main rendering engine. But you could just as easily use individual components linked together with some kind of event driven framework.

这篇关于如何更改actionPerformed()中的Swing Timer Delay的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆