Android: CountDownTimer пропускает последний onTick ()! - PullRequest
44 голосов
/ 14 января 2012

код:

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, которому присвоен идентификатор tv , а для текста установлено значение "".

Ответы [ 12 ]

50 голосов
/ 05 сентября 2012

Я проверил исходный код CountDownTimer. «Пропущенный тик» происходит от специальной функции CountDownTimer, которую я еще не видел в других документах:

В начале каждого тика, до вызова onTick (), вычисляется оставшееся время до окончания обратного отсчета. Если это время меньше интервала обратного отсчета, onTick больше не вызывается , а . Вместо этого запланирован только следующий тик (где будет вызываться метод onFinish ()).

Учитывая тот факт, что аппаратные часы не всегда являются сверхточными, что в фоновом режиме могут быть другие процессы, которые задерживают поток, выполняющий CountDownTimer, плюс то, что сам Android, вероятно, создаст небольшую задержку при вызове обработчика сообщений CountDownTimer, это больше чем вероятно, что вызов последнего тика до окончания обратного отсчета будет запаздывать как минимум на одну миллисекунду, и поэтому onTick () вызываться не будет.

Для своего приложения я решил эту проблему, просто уменьшив интервалы между тиками (500 мс)

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

и я мог бы оставить свой код таким, какой он есть. Для приложений, где важна длительность интервала времени, другие решения, опубликованные здесь, вероятно, являются лучшими.

22 голосов
/ 14 января 2012

Я не знаю, почему не работает последний тик, но вы можете создать свой собственный таймер, например, с помощью 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();

РЕДАКТИРОВАТЬ ВОПРОС ГУФИ

у вас должна быть переменная для хранения состояния счетчика (логическое значение). тогда вы можете написать метод Stop (), например Start ().

РЕДАКТИРОВАТЬ-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);
    }
}

main.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>
4 голосов
/ 16 июня 2012

Самое простое решение, которое я придумал, заключается в следующем. Обратите внимание, что это работает, только если вам нужен простой экран для отображения с обратным отсчетом секунд.

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 миллисекунд, но визуально отображаются только секунды.

3 голосов
/ 07 марта 2012

Хотя приведенное выше решение действительно, его можно улучшить.В нем нет необходимости запускать внутри другого класса (который уже может быть обработан сам по себе).Так что просто создайте класс, который расширяет поток (или работает).

    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);
        }
      }
    }
3 голосов
/ 30 января 2012

Я часами пытался разобраться в этой проблеме, и я рад показать вам хорошую работу.Не беспокойтесь о ожидании вызова onFinish(), просто добавьте 1 (или любой другой интервал) к своим единицам, а затем добавьте оператор if в вызовах onTick().Просто выполните ваши 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();
2 голосов
/ 23 мая 2018

Добавьте несколько миллисекунд к вашему таймеру, чтобы дать ему время для обработки кода.Я добавил +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();
}
2 голосов
/ 02 августа 2014

Так что я думаю, что я немного перегружен, потому что мой таймер работает в своем собственном потоке вместо использования обработчиков 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());
        }
    }
}

}

2 голосов
/ 30 апреля 2014

Я нашел простое решение. Мне нужен 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);
    }

};

Маленькая галочка делает трюк:)

1 голос
/ 10 ноября 2017

если ваш интервал времени больше 4 секунд, то каждый onTick() вызов будет неправильным. Поэтому, если вы хотите получить точный результат, оставьте интервал менее 5 секунд. Перезапуск происходит в начале каждого тика, до вызова onTick(), вычисляется оставшееся время до конца обратного отсчета, и если это время меньше интервала времени обратного отсчета, onTick() больше не будет вызываться. Вместо этого запланирован только следующий тик (где будет вызываться метод onFinish()).

1 голос
/ 23 ноября 2016

Чтобы расширить ответ 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
    }
}
...