【问题标题】:Android: CountDownTimer skips last onTick()!Android:CountDownTimer 跳过最后一个 onTick()!
【发布时间】:2012-02-10 01:41:57
【问题描述】:

代码:

public class SMH extends Activity {  

    public void onCreate(Bundle b) {  
        super.onCreate(b);  
        setContentView(R.layout.main);  

        TextView tv = (TextView) findViewById(R.id.tv);  

        new CountDownTimer(10000, 2000) {  
            public void onTick(long m) {  
               long sec = m/1000+1;  
               tv.append(sec+" seconds remain\n");  
            }  
            public void onFinish() {  
               tv.append("Done!");  
            }  
        }.start();  
   }

输出:
还剩 10 秒
还剩 8 秒
还剩 6 秒
还剩 4 秒
完毕!

问题:

如何让它显示“还剩 2 秒”?经过的时间确实是 10 秒,但最后一次 onTick() 从未发生。如果我将第二个参数从 2000 更改为 1000,那么这是输出:

还剩 10 秒
还剩 9 秒
还剩 8 秒
还剩 7 秒
还剩 6 秒
还剩 5 秒
还剩 4 秒
还剩 3 秒
还剩 2 秒
完毕!

所以你看,它似乎跳过了最后一次 onTick() 调用。顺便说一句,XML 文件基本上是默认的 main.xml,TextView 的 ID 为 tv,文本设置为“”。

【问题讨论】:

  • Oreo 已修复此错误
  • 如果有人对向后兼容性感兴趣,虽然问题可能已经在 Android 8 中得到解决,但它存在于 Android 7 中,我还没有看到它按照 OP 认为它应该在 Android 5 中的方式工作或 6.

标签: java android timer countdown countdowntimer


【解决方案1】:

我不知道为什么最后一个刻度不起作用,但您可以使用 Runable 创建自己的计时器。

class MyCountDownTimer {
    private long millisInFuture;
    private long countDownInterval;
    public MyCountDownTimer(long pMillisInFuture, long pCountDownInterval) {
            this.millisInFuture = pMillisInFuture;
            this.countDownInterval = pCountDownInterval;
        }
    public void Start() 
    {
        final Handler handler = new Handler();
        Log.v("status", "starting");
        final Runnable counter = new Runnable(){

            public void run(){
                if(millisInFuture <= 0) {
                    Log.v("status", "done");
                } else {
                    long sec = millisInFuture/1000;
                    Log.v("status", Long.toString(sec) + " seconds remain");
                    millisInFuture -= countDownInterval;
                    handler.postDelayed(this, countDownInterval);
                }
            }
        };

        handler.postDelayed(counter, countDownInterval);
    }
}

然后开始,

new MyCountDownTimer(10000, 2000).Start();

为 GOOFY 的问题编辑

你应该有一个变量来保存计数器状态(布尔值)。然后你可以写一个像 Start() 这样的 Stop() 方法。

针对高飞问题的 EDIT-2

实际上停止计数器没有错误,但停止(恢复)后再次启动有错误。

我正在编写一个新的更新完整代码,我刚刚尝试过并且它正在工作。这是一个基本的计数器,通过开始和停止按钮在屏幕上显示时间。

计数器类

public class MyCountDownTimer {
    private long millisInFuture;
    private long countDownInterval;
    private boolean status;
    public MyCountDownTimer(long pMillisInFuture, long pCountDownInterval) {
            this.millisInFuture = pMillisInFuture;
            this.countDownInterval = pCountDownInterval;
            status = false;
            Initialize();
    }

    public void Stop() {
        status = false;
    }

    public long getCurrentTime() {
        return millisInFuture;
    }

    public void Start() {
        status = true;
    }
    public void Initialize() 
    {
        final Handler handler = new Handler();
        Log.v("status", "starting");
        final Runnable counter = new Runnable(){

            public void run(){
                long sec = millisInFuture/1000;
                if(status) {
                    if(millisInFuture <= 0) {
                        Log.v("status", "done");
                    } else {
                        Log.v("status", Long.toString(sec) + " seconds remain");
                        millisInFuture -= countDownInterval;
                        handler.postDelayed(this, countDownInterval);
                    }
                } else {
                    Log.v("status", Long.toString(sec) + " seconds remain and timer has stopped!");
                    handler.postDelayed(this, countDownInterval);
                }
            }
        };

        handler.postDelayed(counter, countDownInterval);
    }
}

活动类

public class CounterActivity extends Activity {
    /** Called when the activity is first created. */
    TextView timeText;
    Button startBut;
    Button stopBut;
    MyCountDownTimer mycounter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        timeText = (TextView) findViewById(R.id.time);
        startBut = (Button) findViewById(R.id.start);
        stopBut = (Button) findViewById(R.id.stop);
        mycounter = new MyCountDownTimer(20000, 1000);
        RefreshTimer();
    }

    public void StartTimer(View v) {
        Log.v("startbutton", "saymaya basladi");
        mycounter.Start();
    }

    public void StopTimer(View v) {
        Log.v("stopbutton", "durdu");
        mycounter.Stop();
    }

    public void RefreshTimer() 
    {
        final Handler handler = new Handler();
        final Runnable counter = new Runnable(){

            public void run(){
                timeText.setText(Long.toString(mycounter.getCurrentTime()));
                handler.postDelayed(this, 100);
            }
        };

        handler.postDelayed(counter, 100);
    }
}

ma​​in.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:weightSum="1">
    <TextView android:textAppearance="?android:attr/textAppearanceLarge" 
              android:text="TextView" android:layout_height="wrap_content" 
              android:layout_width="wrap_content" 
              android:id="@+id/time">
    </TextView>
    <Button android:text="Start" 
            android:id="@+id/start" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:onClick="StartTimer">
    </Button>
    <Button android:text="Stop" 
            android:id="@+id/stop" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:onClick="StopTimer">
    </Button>
</LinearLayout>

【讨论】:

  • 我想我应该只使用一个处理程序来处理它,但减去我在我的问题中谈论的缺陷,CountDownTimer 是如此简单和完美!我的意思是仅从您和我的代码示例中,您可以看到 CountDownTimer 与 Handler 相比是多么简单和紧凑!要是它真的能正常工作就好了,哈哈。
  • 你是对的,我只是想证明你可以用其他方法来做到这一点。
  • @ocanal 谢谢我正在使用你的代码,但如果我想停止计时器怎么办......请回复
  • @Goofy 实际上停止时它并没有运行,但是恢复它时出现问题。我在我的答案中附加了一个新的完整更新代码,也许它对你有帮助。
  • @ocanal 我怎么联系你
【解决方案2】:

您计算的剩余时间不正确。回调获取任务完成前的毫秒数。

public void onTick(long m) {  
    long sec = m/1000+1;  
    tv.append(sec+" seconds remain\n");  
}  

应该是

public void onTick(long m) {  
    long sec = m/1000;  
    tv.append(sec+" seconds remain\n");  
}

我自己从来没有使用过这个类,但看起来你不会在它开始的那一刻得到回调,这就是为什么它看起来像你缺少一个条目。例如10000 毫秒,每滴答 1000 毫秒,您总共会收到 9 个更新回调,而不是 10 - 9000、8000、7000、6000、5000、4000、3000、2000、1000、完成。

【讨论】:

  • 我做了 +1 以使输出有意义。我在问题中的输出反映了倒计时结束前的实际时间,例如当它说“3秒...... 2秒......完成!”这意味着在“完成!”之前真的还有 2 秒的时间。消息出现了。所以基本上如果我从“long sec = m/1000”中取出“+1”,那么文本会说“2 seconds...1 seconds...完成!”即使它说“1秒”,实际上还有2秒。这听起来有点令人困惑,但如果你试试我的短代码,你就会明白我在说什么。
【解决方案3】:

我花了几个小时试图解决这个问题,我很高兴向您展示一个很好的解决方法。不要费心等待onFinish() 调用,只需将1(或任何你的间隔)添加到你的单位,然后在onTick() 调用中添加一个if 语句。只需在最后一个 onTick() 上执行您的 onFinish() 任务即可。这是我得到的:

    new CountDownTimer( (countDownTimerValue + 1) * 1000, 1000) { //Added 1 to the countdownvalue before turning it into miliseconds by multiplying it by 1000.
        public void onTick(long millisUntilFinished) {

          //We know that the last onTick() happens at 2000ms remaining (skipping the last 1000ms tick for some reason, so just throw in this if statement.
            if (millisUntilFinished < 2005){ 
                //Stuff to do when finished.
            }else{
                mTextField.setText("Time remaining: " + (((millisUntilFinished) / 1000) - 1));  //My textfield is obviously showing the remaining time. Note how I've had to subtrack 1 in order to display the actual time remaining.
            }
        }

        public void onFinish() {
        //This is when the timer actually finishes (which would be about 1000ms later right? Either way, now you can just ignore this entirely.


        }
    }.start();

【讨论】:

  • 我喜欢你的解决方案,但我发现,至少对于 Android >=6,问题是最终的 onTick() 没有被调用,但 onFinish() 仍然在正确的时间被调用。由于我只更新我的 onTick 中的倒计时显示,我只是使用您的解决方案来显示最终更新,但将 onFinish() 动画留在同一位置
【解决方案4】:

虽然上述解决方案有效,但还可以进一步改进。它不必要地在另一个类中有一个 runnable(它已经可以自己处理)。所以只需创建一个扩展线程(或可运行)的类。

    class MyTimer extends Thread {
      private long millisInFuture;
      private long countDownInterval;
      final Handler mHandler = new Handler();

      public MyTimer(long pMillisInFuture, long pCountDownInterval) {
        this.millisInFuture = pMillisInFuture;
        this.countDownInterval = pCountDownInterval;
      }

      public void run() {
        if(millisInFuture <= 0) {
          Log.v("status", "done");
        } else {
          millisInFuture -= countDownInterval;
          mHandler.postDelayed(this, countDownInterval);
        }
      }
    }

【讨论】:

    【解决方案5】:

    我想出的最简单的解决方案如下。请注意,它仅在您需要一个简单的屏幕来显示秒数倒计时时才有效。

    mTimer = new CountDownTimer(5000, 100){
                public void onTick(long millisUntilFinished) {
                    mTimerView.setText(Long.toString(millisUntilFinished/1000));                
                 }
    
                 public void onFinish() {
                     mTimerView.setText("Expired");
                 }
            };
    
            mTimer.start();
    

    在上面的代码中,onTick() 每 100 毫秒调用一次,但视觉上只显示秒数。

    【讨论】:

      【解决方案6】:

      我查看了 CountDownTimer 的源代码。 “丢失的刻度”来自 CountDownTimer 的一个特殊功能,我还没有在其他地方看到过该功能:

      在每个刻度开始时,在调用 onTick() 之前,会计算到倒计时结束的剩余时间。如果此时间小于倒计时时间间隔,则不再调用 onTick。而是只安排下一个滴答声(将调用 onFinish() 方法)。

      鉴于硬件时钟并不总是超级精确,后台可能有其他进程延迟运行 CountDownTimer 的线程加上 Android 本身在调用 CountDownTimer 的消息处理程序时可能会产生一个小的延迟倒计时结束前最后一个滴答的调用很可能会延迟至少一毫秒,因此不会调用 onTick()。

      对于我的应用程序,我通过将滴答间隔“稍微”小一些(500 毫秒)解决了这个问题

          myCountDownTimer = new CountDownTimer(countDownTime, intervalTime - 500) {
                                         ...
          }
      

      我可以保持我的代码不变。对于间隔时间长度至关重要的应用程序,此处发布的其他解决方案可能是最好的。

      【讨论】:

      • 是的,或者你可以说“countDownTime+900”或类似的东西。倒计时时间为 10000 且间隔为 1000,如果每个滴答都能按时准确触发,您将只能看到 10 个滴答。在此示例中添加 900 毫秒不会给它足够的时间来再增加 1 个滴答声,但会给它足够的时间来完成“最后一个”滴答声,并为时钟不精确按计划提供一些余量。 (例如,当最后一个滴答声从开始到“10004”毫秒时,你会错过它,但如果你添加一点填充,那就没问题了)
      • 将刻度从 1000 毫秒更改为 500 毫秒也为我修复了这个问题。
      • 请将@JamieB 的建议添加到您的答案中。这样,即使您的应用程序在抽动之间需要特定的时间,它也会起作用。我注意到我的设备上有 6 毫秒的延迟。所以countDownTime+200在大多数情况下应该可以解决,让用户几乎认不出来
      • 由于“功能”仍然存在于 Android 7 中,我记录了 onTick 以 1000 毫秒的滴答声触发时的剩余时间。倒计时超过 5 秒,最后一个滴答声是在 1959 年。它总是短约 40 毫秒,加上 50 毫秒给出了最后一个滴答声的(错觉)。超过 20 秒,不足 70 毫秒。超过 60 秒,不足 180 毫秒。添加固定时间是一个不适用于可变倒计时长度的组合。在短倒计时的 1000 毫秒滴答声上添加 900 毫秒实际上是一个额外的滴答声,这意味着在短暂的倒计时上最后一个滴答声就像 2 个滴答声,这首先是问题的很大一部分。
      【解决方案7】:

      我找到了简单的解决方案。我需要 CountDown 来更新 ProgressBar,所以我这样做了:

      new CountDownTimer(1000, 100) {
      
          private int counter = 0;
      
          @Override
          public void onTick(long millisUntilFinished) {
              Log.d(LOG_TAG, "Tick: " + millisUntilFinished);
              if (++counter == 10) {
                  timeBar.setProgress(--lenght); // timeBar and lenght defined in calling code
                  counter = 0;
              }
          }
      
      
          @Override
          public void onFinish() {
              Log.d(LOG_TAG, "Finish.");
      
              timeBar.setProgress(0);
          }
      
      };
      

      小勾就可以了 :)

      【讨论】:

        【解决方案8】:

        所以我觉得我有点过火了,因为我的计时器在它自己的线程中运行,而不是使用 postDelay 处理程序,尽管它总是回发到创建它的线程。我也知道我只关心秒,所以它围绕这个想法进行了简化。它还允许您取消它并重新启动它。我没有内置暂停功能,因为这不是我的需要。

        /**
        * Created by MinceMan on 8/2/2014.
        */
        public abstract class SecondCountDownTimer {
        
        private final int seconds;
        private TimerThread timer;
        private final Handler handler;
        
        /**
         * @param secondsToCountDown Total time in seconds you wish this timer to count down.
         */
        public SecondCountDownTimer(int secondsToCountDown) {
            seconds = secondsToCountDown;
            handler = new Handler();
            timer = new TimerThread(secondsToCountDown);
        }
        
        /** This will cancel your current timer and start a new one.
         *  This call will override your timer duration only one time. **/
        public SecondCountDownTimer start(int secondsToCountDown) {
            if (timer.getState() != State.NEW) {
                timer.interrupt();
                timer = new TimerThread(secondsToCountDown);
            }
            timer.start();
            return this;
        }
        
        /** This will cancel your current timer and start a new one. **/
        public SecondCountDownTimer start() {
            return start(seconds);
        }
        
        public void cancel() {
            if (timer.isAlive()) timer.interrupt();
            timer = new TimerThread(seconds);
        }
        
        public abstract void onTick(int secondsUntilFinished);
        private Runnable getOnTickRunnable(final int second) {
            return new Runnable() {
                @Override
                public void run() {
                    onTick(second);
                }
            };
        }
        
        public abstract void onFinish();
        private Runnable getFinishedRunnable() {
            return new Runnable() {
                @Override
                public void run() {
                    onFinish();
                }
            };
        }
        
        private class TimerThread extends Thread {
        
            private int count;
        
            private TimerThread(int count) {
                this.count = count;
            }
        
            @Override
            public void run() {
                try {
                    while (count != 0) {
                        handler.post(getOnTickRunnable(count--));
                        sleep(1000);
                    }
                } catch (InterruptedException e) { }
                if (!isInterrupted()) {
                    handler.post(getFinishedRunnable());
                }
            }
        }
        

        }

        【讨论】:

          【解决方案9】:

          扩展 Nantoka 的答案。这是我的代码,以确保正确更新视图:

          countDownTimer = new CountDownTimer(countDownMsec, 500) 
          {
              public void onTick(long millisUntilFinished)
              {
                  if(millisUntilFinished!=countDownMsec)
                  {
                      completedTick+=1;
                      if(completedTick%2==0)      // 1 second has passed
                      {
                          // UPDATE VIEW HERE based on "seconds = completedTick/2"
                      }
                      countDownMsec = millisUntilFinished;  // store in case of pause
                  }
              }
          
              public void onFinish()
              {
                  countDownMsec = 0;
                  completedTick+=2;       // the final 2 ticks arrive together
                  countDownTimer = null;
          
                  // FINAL UPDATE TO VIEW HERE based on seconds = completedTick/2 == countDownMsec/1000
              }
          }
          

          【讨论】:

            【解决方案10】:

            我在使用 CountDownTimer 时也遇到了同样的问题,我尝试了不同的方法。 所以最简单的方法之一是@Nantoca 提供的解决方案——他建议将频率从 1000 毫秒提高到 500 毫秒。但我不喜欢这个解决方案,因为它会做更多的工作,会消耗一些额外的电池资源。

            所以我决定使用@ocanal 的灵魂并编写自己的简单 CustomCountDownTimer。

            但我在他的代码中发现了几个缺陷:

            1. 效率有点低(创建第二个处理程序来发布结果)

            2. 它开始延迟发布第一个结果。 (你需要在第一次初始化时做一个post()方法而不是postDelayed()

            3. 看起来很奇怪。带有大写字母、状态而不是经典 isCanceled 布尔值等的方法。

            所以我稍微清理了一下,这是他方法的更常见版本:

            private class CustomCountDownTimer {
            
                private Handler mHandler;
                private long millisUntilFinished;
                private long countDownInterval;
                private boolean isCanceled = false;
            
                public CustomCountDownTimer(long millisUntilFinished, long countDownInterval) {
                    this.millisUntilFinished = millisUntilFinished;
                    this.countDownInterval = countDownInterval;
                    mHandler = new Handler();
                }
            
                public synchronized void cancel() {
                    isCanceled = true;
                    mHandler.removeCallbacksAndMessages(null);
                }
            
                public long getRemainingTime() {
                    return millisUntilFinished;
                }
            
                public void start() {
            
                    final Runnable counter = new Runnable() {
            
                        public void run() {
            
                            if (isCanceled) {
                                publishUpdate(0);
                            } else {
            
                                //time is out
                                if(millisUntilFinished <= 0){
                                    publishUpdate(0);
                                    return;
                                }
            
                                //update UI:
                                publishUpdate(millisUntilFinished);
            
                                millisUntilFinished -= countDownInterval;
                                mHandler.postDelayed(this, countDownInterval);
                            }
                        }
                    };
            
                    mHandler.post(counter);
                }
            }
            

            【讨论】:

              【解决方案11】:

              如果您的时间间隔超过 4 秒,那么每个 onTick() 呼叫将不正确。因此,如果您想要精确的结果,请保持间隔小于 5 秒。 Reseaon 在每个刻度的开始,在调用onTick() 之前,计算倒计时结束的剩余时间,如果此时间小于倒计时时间间隔,则不会再调用onTick()。而是只安排下一个滴答声(将调用 onFinish() 方法)。

              【讨论】:

                【解决方案12】:

                给你的计时器增加几毫秒,让它有时间处理代码。 我将+100 添加到您的计时器长度中,还添加了Math.ceil() 以对结果进行四舍五入,而不是添加1。

                另外...第一个刻度是 AFTER 2000 毫秒,因此除非您添加它,否则您不会得到“还剩 10 秒”条目。

                protected void onCreate(Bundle savedInstanceState) {
                    super.onCreate(savedInstanceState);
                    setContentView(R.layout.activity_main);
                
                    final TextView tv = (TextView) findViewById(R.id.tv);
                    tv.setText("10 Seconds remain\n"); //displayed before the first tick.
                    new CountDownTimer(10000+25, 1000) { //25 to account for processing time
                        public void onTick(long m) {
                            long sec = (long) Math.ceil(m / 2000 ); //round up, don't add 1
                            tv.append(sec + " seconds remain\n");
                        }
                        public void onFinish() {
                            tv.append("Done!");
                        }
                    }.start();
                }
                

                【讨论】:

                  猜你喜欢
                  • 2018-09-24
                  • 2019-07-09
                  • 1970-01-01
                  • 1970-01-01
                  • 2023-02-22
                  • 2014-05-24
                  • 2010-11-26
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多