android CountDownTimer - additional milliseconds delay between ticks

Solution 1:

Rewrite

As you said, you also noticed that the next time in onTick() is calculated from the time the previous onTick() ran, which introduces a tiny error on every tick. I changed the CountDownTimer source code to call each onTick() at the specified intervals from the start time.

I build this upon the CountDownTimer framework, so cut & paste the source code into your project and give the class a unique name. (I called mine MoreAccurateTimer.) Now make a few changes:

  1. Add a new class variable:

    private long mNextTime;
    
  2. Change start():

    public synchronized final MoreAccurateTimer start() {
        if (mMillisInFuture <= 0) {
            onFinish();
            return this;
        }
    
        mNextTime = SystemClock.uptimeMillis();
        mStopTimeInFuture = mNextTime + mMillisInFuture;
    
        mNextTime += mCountdownInterval;
        mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG), mNextTime);
        return this;
    }
    
  3. Change the Handler's handlerMessage():

    @Override
    public void handleMessage(Message msg) {
        synchronized (MoreAccurateTimer.this) {
            final long millisLeft = mStopTimeInFuture - SystemClock.uptimeMillis();
    
            if (millisLeft <= 0) {
                onFinish();
            } else {
                onTick(millisLeft);
    
                // Calculate next tick by adding the countdown interval from the original start time
                // If user's onTick() took too long, skip the intervals that were already missed
                long currentTime = SystemClock.uptimeMillis();
                do {
                    mNextTime += mCountdownInterval;
                } while (currentTime > mNextTime);
    
                // Make sure this interval doesn't exceed the stop time
                if(mNextTime < mStopTimeInFuture)
                    sendMessageAtTime(obtainMessage(MSG), mNextTime);
                else
                    sendMessageAtTime(obtainMessage(MSG), mStopTimeInFuture);
            }
        }
    }
    

Solution 2:

So this is what I came up with. It is a small modification of the original CountDownTimer. What it adds is a variable mTickCounter which counts the amount of ticks called. This variable is used together with the new variable mStartTime to see how accurate we are with our ticks. Based on this input the delay to the next tick is adjusted ... It seems to do what I was looking for yet I am sure this can be improved upon.

Look for

// ************AccurateCountdownTimer***************

in the source code to find the modifications I have added to the original class.

package com.dorjeduck.xyz;

import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;

/**
 * Schedule a countdown until a time in the future, with regular notifications
 * on intervals along the way.
 * 
 * Example of showing a 30 second countdown in a text field:
 * 
 * <pre class="prettyprint">
 * new CountDownTimer(30000, 1000) {
 * 
 *  public void onTick(long millisUntilFinished) {
 *      mTextField.setText(&quot;seconds remaining: &quot; + millisUntilFinished / 1000);
 *  }
 * 
 *  public void onFinish() {
 *      mTextField.setText(&quot;done!&quot;);
 *  }
 * }.start();
 * </pre>
 * 
 * The calls to {@link #onTick(long)} are synchronized to this object so that
 * one call to {@link #onTick(long)} won't ever occur before the previous
 * callback is complete. This is only relevant when the implementation of
 * {@link #onTick(long)} takes an amount of time to execute that is significant
 * compared to the countdown interval.
 */
public abstract class AccurateCountDownTimer {

    /**
     * Millis since epoch when alarm should stop.
     */
    private final long mMillisInFuture;

    /**
     * The interval in millis that the user receives callbacks
     */
    private final long mCountdownInterval;

    private long mStopTimeInFuture;

    // ************AccurateCountdownTimer***************
    private int mTickCounter;
    private long mStartTime;

    // ************AccurateCountdownTimer***************

    /**
     * @param millisInFuture
     *            The number of millis in the future from the call to
     *            {@link #start()} until the countdown is done and
     *            {@link #onFinish()} is called.
     * @param countDownInterval
     *            The interval along the way to receive {@link #onTick(long)}
     *            callbacks.
     */
    public AccurateCountDownTimer(long millisInFuture, long countDownInterval) {
        mMillisInFuture = millisInFuture;
        mCountdownInterval = countDownInterval;

        // ************AccurateCountdownTimer***************
        mTickCounter = 0;
        // ************AccurateCountdownTimer***************
    }

    /**
     * Cancel the countdown.
     */
    public final void cancel() {
        mHandler.removeMessages(MSG);
    }

    /**
     * Start the countdown.
     */
    public synchronized final AccurateCountDownTimer start() {
        if (mMillisInFuture <= 0) {
            onFinish();
            return this;
        }

        // ************AccurateCountdownTimer***************
        mStartTime = SystemClock.elapsedRealtime();
        mStopTimeInFuture = mStartTime + mMillisInFuture;
        // ************AccurateCountdownTimer***************

        mHandler.sendMessage(mHandler.obtainMessage(MSG));
        return this;
    }

    /**
     * Callback fired on regular interval.
     * 
     * @param millisUntilFinished
     *            The amount of time until finished.
     */
    public abstract void onTick(long millisUntilFinished);

    /**
     * Callback fired when the time is up.
     */
    public abstract void onFinish();

    private static final int MSG = 1;

    // handles counting down
    private Handler mHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {

            synchronized (AccurateCountDownTimer.this) {
                final long millisLeft = mStopTimeInFuture
                        - SystemClock.elapsedRealtime();

                if (millisLeft <= 0) {
                    onFinish();
                } else if (millisLeft < mCountdownInterval) {
                    // no tick, just delay until done
                    sendMessageDelayed(obtainMessage(MSG), millisLeft);
                } else {
                    long lastTickStart = SystemClock.elapsedRealtime();
                    onTick(millisLeft);

                    // ************AccurateCountdownTimer***************
                    long now = SystemClock.elapsedRealtime();
                    long extraDelay = now - mStartTime - mTickCounter
                            * mCountdownInterval;
                    mTickCounter++;
                    long delay = lastTickStart + mCountdownInterval - now
                            - extraDelay;

                    // ************AccurateCountdownTimer***************

                    // take into account user's onTick taking time to execute

                    // special case: user's onTick took more than interval to
                    // complete, skip to next interval
                    while (delay < 0)
                        delay += mCountdownInterval;

                    sendMessageDelayed(obtainMessage(MSG), delay);
                }
            }
        }
    };
}

Solution 3:

In most systems the timers are never perfectly accurate. The system only executes the timer when it doesn't have anything else to do. If the CPU is busy with a background process or a different thread then it won't get around to calling the timer till it has finished.

You might have better luck changing the interval to something smaller like 100ms and then only redrawing the screen if something has changed. With this approach the timer isn't directly causing anything to occur, it's just redrawing the screen periodically.