Android CountDownTimer - additional delay in milliseconds between ticks

From my observation, the android CountDownTimer countDownInterval between ticks is inaccurate, countDownInterval is regularly several milliseconds longer than indicated. The CountDownInterval in my particular application is 1000 ms, just counting a certain amount of time with one second step.

Because of these lengthy ticks, I end up with fewer ticks than necessary when the countdown timer runs long enough to spin the displayed countdown (a 2-second step occurs at the user interface level when adding extra ms)

Looking at the source of CountDownTimer, it seems that it can twist it, so it corrects this unwanted inaccuracy, but I was wondering if there is already a better CountDownTimer available in the java / android world.

Thank you friends for any pointer ...

+7
source share
6 answers

Rewrite

As you said, you also noticed that the next time in is onTick()calculated from the moment the previous one starts onTick(), which introduces a tiny error at each tick. I changed the source code of CountDownTimer to call each onTick()at the indicated intervals from the moment it was launched.

CountDownTimer, . ( MoreAccurateTimer.) :

  • :

    private long mNextTime;
    
  • 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;
    }
    
  • 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 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);
            }
        }
    }
    
+16

, . CountDownTimer. mTickCounter, . mStartTime, , . , ... , , , , .

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

, , .

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 onTick taking time to execute

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

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

. , . CPU , .

, , 100 , , - . , .

+2

, , . TimedTaskExecutor, x . x , . AsyncTask, Handlers CountdownTimer, . :

package com.example.myapp;

import java.util.concurrent.TimeUnit;

/*
 * MUST RUN IN BACKGROUND THREAD
 */
public class TimedTaskExecutor {
// ------------------------------ FIELDS ------------------------------

/**
 *
 */
private double intervalInMilliseconds;

/**
 *
 */
private IVoidEmptyCallback callback;

/**
 *
 */
private long sleepInterval;

// --------------------------- CONSTRUCTORS ---------------------------

/**
 * @param intervalInMilliseconds
 * @param callback
 */
public TimedTaskExecutor(double intervalInMilliseconds, IVoidEmptyCallback callback, long sleepInterval) {
    this.intervalInMilliseconds = intervalInMilliseconds;
    this.callback = callback;
    this.sleepInterval = sleepInterval;
}

// --------------------- GETTER / SETTER METHODS ---------------------

/**
 * @return
 */
private IVoidEmptyCallback getCallback() {
    return callback;
}

/**
 * @return
 */
private double getIntervalInMilliseconds() {
    return intervalInMilliseconds;
}

/**
 * @return
 */
private long getSleepInterval() {
    return sleepInterval;
}

// -------------------------- OTHER METHODS --------------------------

/**
 *
 */
public void run(ICallback<Boolean> isRunningChecker) {

    long nanosInterval = (long) (getIntervalInMilliseconds() * 1000000);

    Long previousNanos = null;

    while (isRunningChecker.callback()) {

        long nanos = TimeUnit.NANOSECONDS.toNanos(System.nanoTime());

        if (previousNanos == null || (double) (nanos - previousNanos) >= nanosInterval) {

            getCallback().callback();

            if (previousNanos != null) {

                // Removing the difference
                previousNanos = nanos - (nanos - previousNanos - nanosInterval);

            } else {

                previousNanos = nanos;

            }

        }

        if (getSleepInterval() > 0) {

            try {

                Thread.sleep(getSleepInterval());

            } catch (InterruptedException ignore) {
            }

        }

    }

}

// -------------------------- INNER CLASSES --------------------------

/**
 *
 */
public interface IVoidEmptyCallback {
    /**
     *
     */
    public void callback();
}

/**
 * @param <T>
 */
public interface ICallback<T> {
    /**
     * @return
     */
    public T callback();
}

}

, :

private boolean running;

Handler handler = new Handler();

handler.postDelayed(
    new Runnable() {
        /**
         *
         */
        @Override
        public void run() {
            running = false;
        }
    },
    5000
);

HandlerThread handlerThread = new HandlerThread("For background");
handlerThread.start();

Handler background = new Handler(handlerThread.getLooper());

background.post(
    new Runnable() {
        /**
         *
         */
        @Override
        public void run() {

            new TimedTaskExecutor(
                    10, // Run tick every 10 milliseconds
                    // The callback for each tick
                    new TimedTaskExecutor.IVoidEmptyCallback() {
                        /**
                         *
                         */
                        private int counter = 1;

                        /**
                         *
                         */
                        @Override
                        public void callback() {
                            // You can use the handler to post runnables to the UI
                            Log.d("runTimedTask", String.valueOf(counter++));
                        }
                    },
                    // sleep interval in order to allow the CPU to rest
                    2
            ).run(
                    // A callback to check when to stop
                    new TimedTaskExecutor.ICallback<Boolean>() {
                        /**
                         *
                         * @return
                         */
                        @Override
                        public Boolean callback() {
                            return running;
                        }
                    }
            );

        }
    }
 );

500 x. ( )

  • : , Nexus 5 Lollipop 0 .
0

As Nathan Villaecuscus previously wrote, a good approach is to reduce the โ€œcountdownโ€ interval and then run the ACTION required every second, checking to see if it is a new second.

Guys, look at this approach:

new CountDownTimer(5000, 100) {

     int n = 5;
     Toast toast = null;

     @Override
     public void onTick(long l) {

         if(l < n*1000) {
             //
             // new seconds. YOUR ACTIONS
             //
             if(toast != null) toast.cancel();

             toast = Toast.makeText(MainActivity.this, "" + n, Toast.LENGTH_SHORT);
             toast.show();

             // this one is important!!!
             n-=1;
         }

     }

     @Override
     public void onFinish() {
         if(toast != null) toast.cancel();
         Toast.makeText(MainActivity.this, "START", Toast.LENGTH_SHORT).show();
     }
}.start();

Hope this helps someone; p

0
source

I wrote one lib to prevent this phenomenon.
https://github.com/imknown/NoDelayCountDownTimer

Basic usage codes:

private long howLongLeftInMilliSecond = NoDelayCountDownTimer.SIXTY_SECONDS;

private NoDelayCountDownTimer noDelayCountDownTimer;
private TextView noDelayCountDownTimerTv;

NoDelayCountDownTimer noDelayCountDownTimer = new NoDelayCountDownTimerInjector<TextView>(noDelayCountDownTimerTv, howLongLeftInMilliSecond).inject(new NoDelayCountDownTimerInjector.ICountDownTimerCallback() {
    @Override
    public void onTick(long howLongLeft, String howLongSecondLeftInStringFormat) {
        String result = getString(R.string.no_delay_count_down_timer, howLongSecondLeftInStringFormat);

        noDelayCountDownTimerTv.setText(result);
    }

    @Override
    public void onFinish() {
        noDelayCountDownTimerTv.setText(R.string.finishing_counting_down);
    }
});

The main basic logical cod:

private Handler mHandler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {

        synchronized (NoDelayCountDownTimer.this) {
            if (mCancelled) {
                return true;
            }

            final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();

            if (millisLeft <= 0 || millisLeft < mCountdownInterval) {
                onFinish();
            } else {
                long lastTickStart = SystemClock.elapsedRealtime();
                onTick(millisLeft);

                // take into account user onTick taking time to execute
                long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();

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

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

        return true;
    }
});

record

-2
source

All Articles