Use and Encapsulation of CountDownTimer for Android Countdown and Improvement

Keywords: Android Apache

(1) Introduction

Official documents. CountDownTimer is a countdown class. You can also specify a time interval for regular notification. Take a chestnut, for example, if your countdown is 100 seconds, you can specify every 20 seconds. This will be called back at the beginning, once in 20 seconds, once in 40 seconds. The callbacks at 200 seconds and at intervals are different.

(2) Use

CountDownTimer has only one construct.

/**
     * @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 CountDownTimer(long millisInFuture, long countDownInterval) {
         mMillisInFuture = millisInFuture;
         mCountdownInterval = countDownInterval;
     }

As can be seen from the method annotations, the first parameter is the total countdown time, and the second parameter is the periodic callback time.

When using:

new CountDownTimer(10000, 2000) {
    @Override
    public void onTick(long millisUntilFinished) {
        Log.v("CountDownTimerTest", "onTick millisUntilFinished = " + millisUntilFinished);
    }
    @Override
    public void onFinish() {
        Log.v("CountDownTimerTest", "onFinish");
    }
}.start();

The onTick() method is a periodic interval callback method, and onFinish() is the end callback method.


You can see that the onTick() method is called back once in 9970 milliseconds, because messaging takes a little time. It takes 9976 milliseconds to use the onTick() method when it comes in, and the next one is, where the onTick() method is executed four times, shouldn't the onTick() method be executed again with the remaining two seconds? How come the onTick() method is not implemented after 3964? The reason is that after 2 seconds of 3964 milliseconds, more than 1900 milliseconds are left, which is smaller than 2 seconds, using the onTick() method without execution.

Core Code of CountDownTimer

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);
    long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
    while (delay < 0) delay += mCountdownInterval;
    sendMessageDelayed(obtainMessage(MSG), delay);
}

If the countdown is not executed after the countdown, the countdown can also be cancelled by calling CountDownTimer's cancel() method.

(3) Packaging

Sometimes we don't care about onTick(), just about the execution of onFinish() method. We can encapsulate a tool class and separate the two functions. We can use them separately when we want to use them alone and when we want to use them together, we can use them together.

package com.ce.countdowntimertest.utils;
import android.os.CountDownTimer;
/**
 * Countdown Tool Class
 */
public class CountDownTimerUtils {
    /**
     * Callback interface at the end of countdown
     */
    public interface FinishDelegate {
        void onFinish();
    }
    /**
     * Interface for regular callbacks
     */
    public interface TickDelegate {
        void onTick(long pMillisUntilFinished);
    }
    private final static long ONE_SECOND = 1000;
    /**
     * Total countdown time
     */
    private long mMillisInFuture = 0;
    /**
     * Periodic callback time must be greater than 0 or ANR will occur
     */
    private long mCountDownInterval;
    /**
     * Callback at the end of countdown
     */
    private FinishDelegate mFinishDelegate;
    /**
     * Regular callbacks
     */
    private TickDelegate mTickDelegate;
    private MyCountDownTimer mCountDownTimer;
    /**
     * Get CountDownTimerUtils
     * @return CountDownTimerUtils
     */
    public static CountDownTimerUtils getCountDownTimer() {
        return new CountDownTimerUtils();
    }
    /**
     * Set the time call for periodic callbacks {@link# setTickDelegate (TickDelegate)}
     * @param pCountDownInterval Periodic callback time must be greater than 0
     * @return CountDownTimerUtils
     */
    public CountDownTimerUtils setCountDownInterval(long pCountDownInterval) {
        this.mCountDownInterval=pCountDownInterval;
        return this;
    }
    /**
     * Set the callback at the end of the countdown
     * @param pFinishDelegate Callback interface at the end of countdown
     * @return CountDownTimerUtils
     */
    public CountDownTimerUtils setFinishDelegate(FinishDelegate pFinishDelegate) {
        this.mFinishDelegate=pFinishDelegate;
        return this;
    }
    /**
     * Setting the total countdown time
     * @param pMillisInFuture Total countdown time
     * @return CountDownTimerUtils
     */
    public CountDownTimerUtils setMillisInFuture(long pMillisInFuture) {
        this.mMillisInFuture=pMillisInFuture;
        return this;
    }
    /**
     * Setting up regular callbacks
     * @param pTickDelegate Periodic callback interface
     * @return CountDownTimerUtils
     */
    public CountDownTimerUtils setTickDelegate(TickDelegate pTickDelegate) {
        this.mTickDelegate=pTickDelegate;
        return this;
    }
    public void create() {
        if (mCountDownTimer != null) {
            mCountDownTimer.cancel();
            mCountDownTimer = null;
        }
        if (mCountDownInterval <= 0) {
            mCountDownInterval = mMillisInFuture + ONE_SECOND;
        }
        mCountDownTimer = new MyCountDownTimer(mMillisInFuture, mCountDownInterval);
        mCountDownTimer.setTickDelegate(mTickDelegate);
        mCountDownTimer.setFinishDelegate(mFinishDelegate);
    }
    /**
     * Start countdown
     */
    public void start() {
        if (mCountDownTimer == null) {
            create();
        }
        mCountDownTimer.start();
    }
    /**
     * Cancel Countdown
     */
    public void cancel() {
        if (mCountDownTimer != null) {
            mCountDownTimer.cancel();
        }
    }
    private static class MyCountDownTimer extends CountDownTimer {
        private FinishDelegate mFinishDelegate;
        private TickDelegate mTickDelegate;
        /**
         * @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 MyCountDownTimer(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }
        @Override
        public void onTick(long millisUntilFinished) {
            if (mTickDelegate != null) {
                mTickDelegate.onTick(millisUntilFinished);
            }
        }
        @Override
        public void onFinish() {
            if (mFinishDelegate != null) {
                mFinishDelegate.onFinish();
            }
        }
        void setFinishDelegate(FinishDelegate pFinishDelegate) {
            this.mFinishDelegate=pFinishDelegate;
        }
        void setTickDelegate(TickDelegate pTickDelegate) {
            this.mTickDelegate=pTickDelegate;
        }
    }
}

It is also simple to use getCountDownTimer() method to get an instance; setMillisInFuture() method to set the total countdown time; setFinish Delegate () method to set the callback completed by the countdown; setCountDownInterval() method to set the interval of periodic callbacks, but the value is greater than 0, otherwise ANR will occur, because the continuous callback will cause the Looper message processing to fail; The kDelegate () method sets periodic callbacks.
Only the use of the final countdown is required:

Log.v("CountDownTimerTest", "Start");
        CountDownTimerUtils.getCountDownTimer()
                .setMillisInFuture(5000)
                .setFinishDelegate(new CountDownTimerUtils.FinishDelegate() {
                    @Override
                    public void onFinish() {
                        Log.v("CountDownTimerTest", "onFinish");
                    }
                }).start();



You can also call back at regular intervals, but you need to set the total countdown time:

Log.v("CountDownTimerTest", "Start");
        CountDownTimerUtils.getCountDownTimer()
                .setMillisInFuture(10000)
                .setCountDownInterval(2000)
                .setTickDelegate(new CountDownTimerUtils.TickDelegate() {
                    @Override
                    public void onTick(long pMillisUntilFinished) {
                        Log.v("CountDownTimerTest", "pMillisUntilFinished = " + pMillisUntilFinished);
                    }
                }).start();



Of course, there can be both:

Log.v("CountDownTimerTest", "Start");
        CountDownTimerUtils.getCountDownTimer()
                .setMillisInFuture(10000)
                .setCountDownInterval(2000)
                .setTickDelegate(new CountDownTimerUtils.TickDelegate() {
                    @Override
                    public void onTick(long pMillisUntilFinished) {
                        Log.v("CountDownTimerTest", "pMillisUntilFinished = " + pMillisUntilFinished);
                    }
                })
                .setFinishDelegate(new CountDownTimerUtils.FinishDelegate() {
                    @Override
                    public void onFinish() {
                        Log.v("CountDownTimerTest", "onFinish");
                    }
                }).start();      


(4) Improvement

Although CountDownTimer is very useful, it has the disadvantage that it can only run in the main thread, and if it runs in the sub-thread, it will report an error.

new Thread(new Runnable() {
            @Override
            public void run() {
                Log.v("CountDownTimerTest", "SubThread Start");
                new android.os.CountDownTimer(10000, 2000) {
                    @Override
                    public void onTick(long millisUntilFinished) {
                        Log.v("CountDownTimerTest", "onTick millisUntilFinished = " + millisUntilFinished);
                    }

                    @Override
                    public void onFinish() {
                        Log.v("CountDownTimerTest", "onFinish");
                    }
                }.start();
            }
        }).start();



The reason is that Handler's Looper Android system in the main thread has already helped us prepare(prepareMainLooper()) at the framework level, but there is no Looper in our sub-thread, so what? With the help of HandlerThread, we can create our own CountDownTimer class, copy the source code of Android CountDownTimer to our new CountDownTimer class, modify the package name, instead of using Android CountDownTimer class, we can transform it as follows:

package com.ce.countdowntimertest.common;

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;

/**
 *
 * Created by CE
 *
 * 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("seconds remaining: " + millisUntilFinished / 1000);
 *     }
 *
 *     public void onFinish() {
 *         mTextField.setText("done!");
 *     }
 *  }.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 CountDownTimer {

    /**
     * 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;

    /**
     * boolean representing if the timer was cancelled
     */
    private boolean mCancelled = false;

    /**
     * @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 CountDownTimer(long millisInFuture, long countDownInterval) {
        mMillisInFuture = millisInFuture;
        mCountdownInterval = countDownInterval;
        if (!isMainThread()) {
            mHandlerThread = new HandlerThread("CountDownTimerThread");
            mHandlerThread.start();
            mHandler = new Handler(mHandlerThread.getLooper(), mCallback);
        } else {
            mHandler = new Handler(mCallback);
        }
    }

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

    /**
     * Start the countdown.
     */
    public synchronized final CountDownTimer start() {
        mCancelled = false;
        if (mMillisInFuture <= 0) {
            onFinish();
            return this;
        }
        mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
        mHandler.sendMessage(mHandler.obtainMessage(MSG));
        return this;
    }


    private boolean isMainThread() {
        return Looper.getMainLooper().getThread().equals(Thread.currentThread());
    }

    /**
     * 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 android.os.Handler mHandler = new android.os.Handler() {

        @Override
        public void handleMessage(Message msg) {

            synchronized (CountDownTimer.this) {
                if (mCancelled) {
                    return;
                }

                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);

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

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

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

    private HandlerThread mHandlerThread;
    private Handler mHandler;

    private Handler.Callback mCallback = new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            synchronized (CountDownTimer.this) {
                if (mCancelled) {
                    return true;
                }
                final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();
                if (millisLeft <= 0) {
                    onFinish();
                    if (mHandlerThread != null) mHandlerThread.quit();
                } else if (millisLeft < mCountdownInterval) {
                    // no tick, just delay until done
                    mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG), millisLeft);
                } else {
                    long lastTickStart = SystemClock.elapsedRealtime();
                    onTick(millisLeft);
                    // take into account user's onTick taking time to execute
                    long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
                    // special case: user's onTick took more than interval to
                    // complete, skip to next interval
                    while (delay < 0) delay += mCountdownInterval;
                    mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG), delay);
                }
            }
            return false;
        }
    };
}

If it's the main thread, it doesn't matter. If it's the sub-thread, we'll take the getLooper() of HandlerThread out to Handler. Here I use Handler.Callback for message processing. Now let's change the CountDownTimer class that we just ran in the sub-thread to the CountDownTimer class that we defined ourselves.

new Thread(new Runnable() {
    @Override
    public void run() {
        Log.v("CountDownTimerTest", "SubThread Start");
        new com.ce.countdowntimertest.common.CountDownTimer(10000, 2000) {
            @Override
            public void onTick(long millisUntilFinished) {
                Log.v("CountDownTimerTest", "onTick millisUntilFinished = " + millisUntilFinished);
            }

            @Override
            public void onFinish() {
                Log.v("CountDownTimerTest", "onFinish");
            }
        }.start();
    }
}).start();



It can run, of course, in the main thread is normal:

Log.v("CountDownTimerTest", "Start");
        new com.ce.countdowntimertest.common.CountDownTimer(10000, 2000) {
            @Override
            public void onTick(long millisUntilFinished) {
                Log.v("CountDownTimerTest", "onTick millisUntilFinished = " + millisUntilFinished);
            }

            @Override
            public void onFinish() {
                Log.v("CountDownTimerTest", "onFinish");
            }
        }.start();



If you want to use this improved CountDownTimer in the CountDownTimerUtils class, change the package name of CountDownTimer to run.

Source address

Posted by Templar on Fri, 12 Apr 2019 01:33:32 -0700