问题:创建一个非常准确的Swing Timer

为了使SwingTimer准确,我喜欢@Tony Docherty On CR建议的逻辑和示例。 这是链接 。

为了突出显示给定的单词,一次又一次,总会有几微秒的延迟。 如果我有话要强调说:“你好怎么样”,每个单词的值分别是(延迟):200,300,400 ms,那么计时器的实际时间总是更多。 说而不是200毫秒,它需要216毫秒。 像这样,如果我有很多话……最后,额外的延迟是显而易见的。

我必须强调每个字母说:’h”””””0’每个应该得到200 /长度(即5)=约40毫秒。 设置每个字母后的延迟。

我的逻辑是,在开始这个过程之前,先把当前时间说成startTime 。 另外,计算totalDelay ,即totalDelay + = delay / .length()。

现在检查条件:( startTime+totalDelay-System.currentTime )如果这是-ve,这意味着时间消耗更多,所以跳过这封信。 检查直到有一个正延迟。这意味着我要添加时间直到现在,并用它开始时的过程所花费的时间差来检查它。

这可能导致跳过以突出显示字母。

但有些事情是错的。 什么,我很难搞清楚。 这可能是循环事情的一些问题。 我已经看到它进入循环(检查时间是否为-ve)只是两次。 但事实并非如此。 而且我也不确定是否会设置下一次延迟。 有任何想法吗?

这是一个SSCCE:

  import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.lang.reflect.InvocationTargetException; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JTextPane; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; public class Reminder { private static final String TEXT = "arey chod chaad ke apnee saleem ki gali anarkali disco chalo"; private static final String[] WORDS = TEXT.split(" "); private JFrame frame; private Timer timer; private StyledDocument doc; private JTextPane textpane; private int[] times = new int[100]; private long totalDelay=0,startTime=0; private int stringIndex = 0; private int index = 0; public void startColoring() { times[0]=100;times[9]=200;times[10]=200;times[11]=200;times[12]=200; times[1]=400;times[2]=300;times[3]=900;times[4]=1000;times[5]=600;times[6]=200;times[7]=700;times[8]=700; ActionListener actionListener = new ActionListener() { @Override public void actionPerformed(ActionEvent actionEvent) { doc.setCharacterAttributes(stringIndex, 1, textpane.getStyle("Red"), true); stringIndex++; try { if (stringIndex >= doc.getLength() || doc.getText(stringIndex, 1).equals(" ")|| doc.getText(stringIndex, 1).equals("\n")) { index++; } if (index < WORDS.length) { double delay = times[index]; totalDelay+=delay/WORDS[index].length(); /*Check if there is no -ve delay, and you are running according to the time*/ /*The problem is here I think. It's just entered this twice*/ while(totalDelay+startTime-System.currentTimeMillis()= doc.getLength() || doc.getText(stringIndex, 1).equals(" ") || doc.getText(stringIndex, 1).equals("\n")) { index += 1; totalDelay+=delay/WORDS[index].length(); } } timer.setDelay((int)(totalDelay+startTime-System.currentTimeMillis())); } else { timer.stop(); System.err.println("Timer stopped"); } } catch (BadLocationException e) { e.printStackTrace(); } } }; startTime=System.currentTimeMillis(); timer = new Timer(times[index], actionListener); timer.setInitialDelay(0); timer.start(); } public void initUI() { frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JPanel panel = new JPanel(); doc = new DefaultStyledDocument(); textpane = new JTextPane(doc); textpane.setText(TEXT); javax.swing.text.Style style = textpane.addStyle("Red", null); StyleConstants.setForeground(style, Color.RED); panel.add(textpane); frame.add(panel); frame.pack(); frame.setVisible(true); } public static void main(String args[]) throws InterruptedException, InvocationTargetException { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { Reminder reminder = new Reminder(); reminder.initUI(); reminder.startColoring(); } }); } } 

更新:

为了更好地理解:

@Tony Docherty给出的EG:

让我们用“测试”这个词说它需要突出显示1秒,因此每个字母突出显示250毫秒。 以你原来的方式做事,确实意味着你为每个字母设置一个250ms的定时器,但是如果每个周期实际上需要260ms并且让’e’周期花费400ms(可能是由于GC或其他使用CPU周期)结束这个词你需要比你应该的时间多180分钟。 将继续为每个单词构建此错误,直到错误如此之大,突出显示不再在视觉上同步。

我正在尝试的方式,而不是反复说这个字母需要突出显示x个时间量,计算每个字母相对于序列开头的时间,即T = 250,e = 500,s = 750,t = 1000。

因此,要获得实际的时间延迟,您需要添加开始时间并减去当前时间。 使用上面给出的时间来运行示例:

 StartTime Letter Offset CurrentTime Delay ActualTimeTaken 100000 T 250 100010 240 250 100000 e 500 100260 240 400 100000 s 750 100660 90 100 100000 t 1000 100760 240 250 

所以你现在应该能够看到每个字母的时间被调整,以考虑到前一个字母的任何超时时间。 当然,时间超限可能很大,你必须跳过突出显示下一个字母(或者可能超过1),但至少我将保持大致同步。

编辑SSCCE

UPDATE2

在第一阶段,我采用每个单词的时间。 也就是说,当用户按下ESC键时,会为特定单词存储时间(他在背景中播放歌曲时会这样做。)按下ESC键时,当前单词会突出显示,当前单词会占用当前单词。 word存储在一个数组中。 我一直在存储时间。 当用户结束时,现在我想根据设置的时间突出显示单词。 所以在这里,用户的时机非常重要。 如果时间很快,那么单词的突出显示也会很快,反之亦然。

新更新:进度

下面的答案有不同的逻辑,但令我惊讶的是,它们或多或少相同。 我发现所有逻辑(包括我的)的一个非常非常奇怪的问题是它们似乎完美地用于几行,但在那之后它们获得了速度,这也不是很慢,但是有很大的差异。

此外,如果您认为我应该以不同的方式思考,您的建议将受到高度赞赏。

好的,所以我一直在查看一些代码(我在上一期关于卡拉OK计时器的问题中发布的代码)

使用该代码我通过System.out.println()使用System.nanoTime()建立一些测量系统,这将帮助我们看到发生了什么:

 import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JTextPane; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.text.Style; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; public class KaraokeTest { private int[] timingsArray = {1000, 1000, 9000, 1000, 1000, 1000, 1000, 1000, 1000, 1000};//word/letters timings private String[] individualWordsToHighlight = {" \nHello\n", " world\n", " Hello", " world", " Hello", " world", " Hello", " world", " Hello", " world"};//each individual word/letters to highlight private int count = 0; private final JTextPane jtp = new JTextPane(); private final JButton startButton = new JButton("Start"); private final JFrame frame = new JFrame(); //create Arrays of individual letters and their timings final ArrayList chars = new ArrayList<>(); final ArrayList charsTiming = new ArrayList<>(); public KaraokeTest() { initComponents(); } private void initComponents() { frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setResizable(false); for (String s : individualWordsToHighlight) { String tmp = jtp.getText(); jtp.setText(tmp + s); } jtp.setEditable(false); startButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { startButton.setEnabled(false); count = 0; charsTiming.clear(); chars.clear(); for (String s : individualWordsToHighlight) { for (int i = 0; i < s.length(); i++) { chars.add(String.valueOf(s.charAt(i))); //System.out.println(String.valueOf(s.charAt(i))); } } //calculate each letters timings for (int x = 0; x < timingsArray.length; x++) { for (int i = 0; i < individualWordsToHighlight[x].length(); i++) { individualWordsToHighlight[x] = individualWordsToHighlight[x].replace("\n", " ").replace("\r", " ");//replace line breaks charsTiming.add((long) (timingsArray[x] / individualWordsToHighlight[x].trim().length()));//dont count spaces //System.out.println(timingsArray[x] / individualWordsToHighlight[x].length()); } } Timer t = new Timer(1, new AbstractAction() { long startTime = 0; long acum = 0; long timeItTookTotal = 0; long dif = 0, timeItTook = 0, timeToTake = 0; int delay = 0; @Override public void actionPerformed(ActionEvent ae) { if (count < charsTiming.size()) { if (count == 0) { startTime = System.nanoTime(); System.out.println("Started: " + startTime); } timeToTake = charsTiming.get(count); acum += timeToTake; //highlight the next word highlightNextWord(); //System.out.println("Acum " + acum); timeItTook = (acum - ((System.nanoTime() - startTime) / 1000000)); timeItTookTotal += timeItTook; //System.out.println("Elapsed since start: " + (System.nanoTime() - startTime)); System.out.println("Time the char should take: " + timeToTake); System.out.println("Time it took: " + timeItTook); dif = (timeToTake - timeItTook); System.out.println("Difference: " + dif); //System.out.println("Difference2 " + (timeToTake - dif)); //calculate start of next letter to highlight less the difference it took between time it took and time it should actually take delay = (int) (timeToTake - dif); if (delay < 1) { delay = 1; } //restart timer with new timings ((Timer) ae.getSource()).setInitialDelay((int) timeToTake);//timer is usually faster thus the entire highlighting will be done too fast //((Timer) ae.getSource()).setInitialDelay(delay); ((Timer) ae.getSource()).restart(); } else {//we are at the end of the array long timeStopped = System.nanoTime(); System.out.println("Stopped: " + timeStopped); System.out.println("Time it should take in total: " + acum); System.out.println("Time it took using accumulator of time taken for each letter: " + timeItTookTotal + "\nDifference: " + (acum - timeItTookTotal)); long timeItTookUsingNanoTime = ((timeStopped - startTime) / 1000000); System.out.println("Time it took using difference (endTime-startTime): " + timeItTookUsingNanoTime + "\nDifference: " + (acum - timeItTookUsingNanoTime)); reset(); ((Timer) ae.getSource()).stop();//stop the timer } count++;//increment counter } }); t.setRepeats(false); t.start(); } }); frame.add(jtp, BorderLayout.CENTER); frame.add(startButton, BorderLayout.SOUTH); frame.pack(); frame.setVisible(true); } private void reset() { startButton.setEnabled(true); jtp.setText(""); for (String s : individualWordsToHighlight) { String tmp = jtp.getText(); jtp.setText(tmp + s); } JOptionPane.showMessageDialog(frame, "Done"); } private void highlightNextWord() { //we still have words to highlight int sp = 0; for (int i = 0; i < count + 1; i++) {//get count for number of letters in words (we add 1 because counter is only incrementd after this method is called) sp += 1; } while (chars.get(sp - 1).equals(" ")) { sp += 1; count++; } //highlight words Style style = jtp.addStyle("RED", null); StyleConstants.setForeground(style, Color.RED); ((StyledDocument) jtp.getDocument()).setCharacterAttributes(0, sp, style, true); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { new KaraokeTest(); } }); } } 

我电脑上的输出是:

开始于:10289712615974

char应该采取的时间:166

花时间:165

差异1

...

char应该采取的时间:166

花时间:155

差异11

...

char应该采取的时间:166

花时间:5

差异161

停止:10299835063084

总时间应该是:9960

每个字母使用累加器所花费的时间:5542

差异:4418

使用差异花费的时间(endTime-startTime):10122

差异:-162

因此,我的结论是Swing Timer实际上运行速度比我们预期的要快,因为Timer s actionPerformed的代码不一定会占用预期突出显示时间的字母,这当然会导致雪崩效应,即定时器运行的速度越快/越慢更大/更小的差异将变为和restart(..)下次执行的计时器将在不同的时间,即更快或更慢。

在代码中执行此操作:

 //calculate start of next letter to highlight less the difference it took between time it took and time it should actually take delay = (int) (timeToTake - dif); //restart timer with new timings //((Timer) ae.getSource()).setInitialDelay((int)timeToTake);//timer is usually faster thus the entire highlighting will be done too fast ((Timer) ae.getSource()).setInitialDelay(delay); ((Timer) ae.getSource()).restart(); 

产生更准确的结果(Ive的最大延迟是每个字母快4ms):

开始于:10813491256556

char应该采取的时间:166

花时间:164

差异2

...

char应该采取的时间:166

花时间:164

差异2

...

char应该采取的时间:166

花时间:162

差异4

停止:10823452105363

总时间应该是:9960

每个字母使用累加器所花费的时间:9806

差异:154

使用差异花费的时间(endTime-startTime):9960

差异:0

我认为要做这样的事情,你需要一个以恒定速率(例如15毫秒)打勾的Swing Timer,只要它足够快以允许你需要的时间粒度,然后在计时器内跳转所需的行为。经过的时间是您需要的时间。

  • 换句话说,根本不要改变定时器的延迟,而只是根据需要改变所需的经过时间。
  • 你不应该在EDT上有一个while (true)循环。 让“while循环”成为Swing Timer本身。
  • 为了使您的逻辑更加简单,您需要检查经过的时间是否> =所需的时间。
  • 同样,不要设置Timer的延迟。 换句话说,不要将它用作计时器而应用作轮询器 。 让它每隔xx毫秒不断地轮询经过的时间,然后如果经过的时间> =你的需要就做出反应。

我建议的代码看起来像这样:

  public void actionPerformed(ActionEvent actionEvent) { if (index > WORDS.length || stringIndex >= doc.getLength()) { ((Timer)actionEvent.getSource()).stop(); } currentElapsedTime = calcCurrentElapsedTime(); if (currentElapsedTime >= elapsedTimeForNextChar) { setNextCharAttrib(stringIndex); stringIndex++; if (atNextWord(stringIndex)) { stringIndex++; // skip whitespace deltaTimeForEachChar = calcNextCharDeltaForNextWord(); } else { elapsedTimeForNextChar += deltaTimeForEachChar; } } // else -- we haven't reached the next time to change char attribute yet. // keep polling. } 

例如,我的SSCCE:

 import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.LinkedList; import java.util.List; import javax.swing.*; import javax.swing.text.*; public class Reminder3 { private static final String TEXT = "arey chod chaad ke apnee saleem ki gali anarkali disco chalo"; private static final String[] WORDS = TEXT.split(" "); private static final int[] TIMES = { 100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200, 200, 200 }; private static final int POLLING_TIME = 12; private StyledDocument doc; private JTextPane textpane; private JPanel mainPanel = new JPanel(); private List reminderWordList = new LinkedList(); private Timer timer; // private int stringIndex = 0; public Reminder3() { doc = new DefaultStyledDocument(); textpane = new JTextPane(doc); textpane.setText(TEXT); javax.swing.text.Style style = textpane.addStyle("Red", null); StyleConstants.setForeground(style, Color.RED); JPanel textPanePanel = new JPanel(); textPanePanel.add(new JScrollPane(textpane)); JButton startBtn = new JButton(new AbstractAction("Start") { @Override public void actionPerformed(ActionEvent arg0) { goThroughWords(); } }); JPanel btnPanel = new JPanel(); btnPanel.add(startBtn); mainPanel.setLayout(new BorderLayout()); mainPanel.add(textPanePanel, BorderLayout.CENTER); mainPanel.add(btnPanel, BorderLayout.SOUTH); } public void goThroughWords() { if (timer != null && timer.isRunning()) { return; } doc = new DefaultStyledDocument(); textpane.setDocument(doc); textpane.setText(TEXT); javax.swing.text.Style style = textpane.addStyle("Red", null); StyleConstants.setForeground(style, Color.RED); int wordStartTime = 0; for (int i = 0; i < WORDS.length; i++) { if (i > 0) { wordStartTime += TIMES[i - 1]; } int startIndexPosition = 0; // set this later ReminderWord reminderWord = new ReminderWord(WORDS[i], TIMES[i], wordStartTime, startIndexPosition); reminderWordList.add(reminderWord); } int findWordIndex = 0; for (ReminderWord word : reminderWordList) { findWordIndex = TEXT.indexOf(word.getWord(), findWordIndex); word.setStartIndexPosition(findWordIndex); findWordIndex += word.getWord().length(); } timer = new Timer(POLLING_TIME, new TimerListener()); timer.start(); } public JComponent getMainPanel() { return mainPanel; } private void setNextCharAttrib(int textIndex) { doc.setCharacterAttributes(textIndex, 1, textpane.getStyle("Red"), true); } private class TimerListener implements ActionListener { private ReminderWord currentWord = null; private long startTime = System.currentTimeMillis(); @Override public void actionPerformed(ActionEvent e) { if (reminderWordList == null) { ((Timer) e.getSource()).stop(); return; } if (reminderWordList.isEmpty() && currentWord.atEnd()) { ((Timer) e.getSource()).stop(); return; } // if just starting, or if done with current word if (currentWord == null || currentWord.atEnd()) { currentWord = reminderWordList.remove(0); // get next word } long totalElapsedTime = System.currentTimeMillis() - startTime; if (totalElapsedTime > (currentWord.getStartElapsedTime() + currentWord .getIndex() * currentWord.getTimePerChar())) { setNextCharAttrib(currentWord.getStartIndexPosition() + currentWord.getIndex()); currentWord.increment(); } } } private static void createAndShowGui() { Reminder3 reminder = new Reminder3(); JFrame frame = new JFrame("Reminder"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(reminder.getMainPanel()); frame.pack(); frame.setLocationByPlatform(true); frame.setVisible(true); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { createAndShowGui(); } }); } } class ReminderWord { private String word; private int totalTime; private int timePerChar; private int startTime; private int startIndexPosition; private int index = 0; public ReminderWord(String word, int totalTime, int startTime, int startIndexPosition) { this.word = word; this.totalTime = totalTime; this.startTime = startTime; timePerChar = totalTime / word.length(); this.startIndexPosition = startIndexPosition; } public String getWord() { return word; } public int getTotalTime() { return totalTime; } public int getStartElapsedTime() { return startTime; } public int getTimePerChar() { return timePerChar; } public int getStartIndexPosition() { return startIndexPosition; } public int increment() { index++; return index; } public int getIndex() { return index; } public boolean atEnd() { return index > word.length(); } public void setStartIndexPosition(int startIndexPosition) { this.startIndexPosition = startIndexPosition; } @Override public String toString() { return "ReminderWord [word=" + word + ", totalTime=" + totalTime + ", timePerChar=" + timePerChar + ", startTime=" + startTime + ", startIndexPosition=" + startIndexPosition + ", index=" + index + "]"; } } 

你考虑过java.util.Timer和scheduleAtFixedRate吗? 在EDT上你需要做一些额外的工作才能完成工作,但它应该解决累积延迟的问题。

ScheduledExecutorService往往比Swing的Timer更准确,它提供了运行多个线程的好处。 特别是,如果一个任务被延迟,它不会影响下一个任务的开始时间(在某种程度上)。

显然,如果任务在EDT上花费的时间太长,这将是你的限制因素。

请参阅下面基于您的SSCCE建议 – 我还稍微重构了startColoring方法并将其拆分为多种方法。 我还添加了一些“日志记录”来获取有关操作时间的反馈。 完成后不要忘记shutdown执行程序,否则可能会阻止程序退出。

每个单词都会稍微延迟着色(在我的机器上5到20毫秒之间),但延迟不是累积的。 您实际上可以测量调度开销并相应地进行调整。

 public class Reminder { private static final String TEXT = "arey chod chaad ke apnee saleem ki gali anarkali disco chalo\n" + "arey chod chaad ke apnee saleem ki gali anarkali disco chalo\n" + "arey chod chaad ke apnee saleem ki gali anarkali disco chalo\n" + "arey chod chaad ke apnee saleem ki gali anarkali disco chalo\n" + "arey chod chaad ke apnee saleem ki gali anarkali disco chalo\n" + "arey chod chaad ke apnee saleem ki gali anarkali disco chalo"; private static final String[] WORDS = TEXT.split("\\s+"); private JFrame frame; private StyledDocument doc; private JTextPane textpane; private static final int[] TIMES = {100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200, 100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200, 100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200, 100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200, 100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200, 100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200, 200}; private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); private int currentLetterIndex; private long start; //for logging public void startColoring() { start = System.currentTimeMillis(); //for logging int startTime = TIMES[0]; for (int i = 0; i < WORDS.length; i++) { scheduler.schedule(colorWord(i, TIMES[i + 1]), startTime, TimeUnit.MILLISECONDS); startTime += TIMES[i+1]; } scheduler.schedule(new Runnable() { @Override public void run() { scheduler.shutdownNow(); } }, startTime, TimeUnit.MILLISECONDS); } //Color the given word, one letter at a time, for the given duration private Runnable colorWord(final int wordIndex, final int duration) { final int durationPerLetter = duration / WORDS[wordIndex].length(); final int wordStartIndex = currentLetterIndex; currentLetterIndex += WORDS[wordIndex].length() + 1; return new Runnable() { @Override public void run() { System.out.println((System.currentTimeMillis() - start) + " ms - Word: " + WORDS[wordIndex] + " - duration = " + duration + "ms"); for (int i = 0; i < WORDS[wordIndex].length(); i++) { scheduler.schedule(colorLetter(wordStartIndex + i), i * durationPerLetter, TimeUnit.MILLISECONDS); } } }; } //Color the letter on the EDT private Runnable colorLetter(final int letterIndex) { return new Runnable() { @Override public void run() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { System.out.println("\t" + (System.currentTimeMillis() - start) + " ms - letter: " + TEXT.charAt(letterIndex)); doc.setCharacterAttributes(letterIndex, 1, textpane.getStyle("Red"), true); } }); } }; } public void initUI() { frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JPanel panel = new JPanel(); doc = new DefaultStyledDocument(); textpane = new JTextPane(doc); textpane.setText(TEXT); javax.swing.text.Style style = textpane.addStyle("Red", null); StyleConstants.setForeground(style, Color.RED); panel.add(textpane); frame.add(panel); frame.pack(); frame.setVisible(true); } public static void main(String args[]) throws InterruptedException, InvocationTargetException { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { Reminder reminder = new Reminder(); reminder.initUI(); reminder.startColoring(); } }); } }