52

代码:

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,文本设置为“”。

4

12 回答 12

57

我检查了 CountDownTimer 的源代码。“丢失的滴答声”来自 CountDownTimer 的一个特殊功能,我还没有在其他地方看到它被记录:

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

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

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

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

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

于 2012-09-05T14:09:53.493 回答
23

我不知道为什么最后一个刻度不起作用,但您可以使用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();

编辑高飞的问题

你应该有一个变量来保存计数器状态(布尔值)。然后你可以编写一个像 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);
    }
}

主要的.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>
于 2012-01-13T23:24:37.517 回答
4

我花了几个小时试图解决这个问题,我很高兴向您展示一个很好的解决方法。不要费心等待onFinish()电话,只需将 1(或任何您的间隔)添加到您的单位,然后在onTick()电话中添加 if 语句。只需在最后一个完成您的onFinish()任务onTick()。这是我所拥有的:

    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();
于 2012-01-30T03:53:42.147 回答
4

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

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

             public void onFinish() {
                 mTimerView.setText("Expired");
             }
        };

        mTimer.start();

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

于 2012-06-16T16:18:04.523 回答
3

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

    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);
        }
      }
    }
于 2012-03-07T03:49:26.980 回答
2

我找到了简单的解决方案。我需要 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);
    }

};

小勾就可以了:)

于 2014-04-30T10:19:43.100 回答
2

所以我觉得我有点过火了,因为我的计时器在它自己的线程中运行,而不是使用 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());
        }
    }
}

}

于 2014-08-02T15:13:35.873 回答
1

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

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
    }
}
于 2016-11-23T10:26:36.933 回答
1

我也遇到了与 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);
    }
}
于 2017-06-11T18:00:34.370 回答
1

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

于 2017-11-10T14:16:47.323 回答
1

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

另外...第一个刻度是在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-05-23T17:37:19.387 回答
-1

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

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,完成。

于 2012-01-14T04:56:09.333 回答