Android将倒计时做到极致

一. 已有倒计时方案存在的问题

在开发倒计时功能时往往我们会为了方便直接使用CountDownTimer或者使用Handler做延时来实现,当然CountDownTimer内部封装也是使用的Handler。

如果只是做次数很少的倒计时或者不需要精确的倒计时逻辑那倒没关系,比如说我只要倒计时10秒,或者我大概5分钟请求某个接口

但是如果是需要做精确的倒计时操作,比如说手机发送验证码60秒,那使用现有的倒计时方案就会存在问题。可能有些朋友没有注意到这一点,下面我们就来简单分析一下现有倒计时的问题。

1. CountDownTimer

这个可能是用得最多的,因为方便嘛。但其实倒计时每一轮倒计时完之后都是存在误差的,如果看过CountDownTimer的源码你就会知道,他的内部是有做校准操作的。(源码很简单这里就不分析了)
但是如果你认真的测试过CountDownTimer,你就会发现,即便它内部有做校准操作,他的没一轮都是有偏差,只是他最后一次倒计时完之后的总共时间和开始倒计时的时间相比没偏差。
什么意思呢,意思就是1秒,2.050秒,3.1秒......,这样的每轮偏差,导致他会出现10.95秒,下一次12秒的情况,那它的回调中如果你直接做取整就会出现少一秒的情况,但实际是没少的。
这只是其中的一个问题,你可以不根据它的回调做展示,自己用一个整形累加做展示也能解决。但是他还有个问题,有概率直接出现跳秒,就是比如3秒,下次直接5秒,这是实际的跳秒,是少了一次回调的那种。
跳秒导致你如果直接使用它可能会大问题,你可能自测的时候没发现,到时一上线应用在用户那概率跳秒,那就蛋疼了。

2. Handler

不搞这么多花里胡哨的,直接使用Handler来实现,会有什么问题。
因为直接使用handler来实现,没有校准操作,每次循环会出现几毫秒的误差,虽然比CountDownTimer的十几毫秒的误差要好,但是在基数大的倒计时情况下误差会累计,导致最终结果和现实时间差几秒误差,时间越久,误差越大

3. Timer

直接使用Timer也一样,只不过他每轮的误差更小,几轮才有1毫秒的误差,但是没有校准还是会出现误差累计,时间越久误差越大。

二. 自己封装倒计时

既然无法直接使用原生的,那我们就自己做一个。
我们基于Handler进行封装,从上面可以看出主要为了解决两个问题,时间校准和跳秒。自己写一个CountDownTimer

public class CountDownTimer {

    private int mTimes;
    private int allTimes;
    private final long mCountDownInterval;
    private final Handler mHandler;
    private OnTimerCallBack mCallBack;
    private boolean isStart;
    private long startTime;

    public CountDownTimer(int times, long countDownInterval){
        this.mTimes = times;
        this.mCountDownInterval = countDownInterval;
        mHandler = new Handler();
    }

    public synchronized void start(OnTimerCallBack callBack){
        this.mCallBack = callBack;
        if (isStart || mCountDownInterval <= 0){
            return;
        }

        isStart = true;
        if (callBack != null){
            callBack.onStart();
        }
        startTime = SystemClock.elapsedRealtime();

        if (mTimes <= 0){
            finishCountDown();
            return;
        }
        allTimes = mTimes;

        mHandler.postDelayed(runnable, mCountDownInterval);
    }

    private final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            mTimes--;
            if (mTimes > 0){
                if (mCallBack != null){
                    mCallBack.onTick(mTimes);
                }

                long nowTime = SystemClock.elapsedRealtime();
                long delay = (nowTime - startTime) - (allTimes - mTimes) * mCountDownInterval;
                // 处理跳秒
                while (delay > mCountDownInterval){
                    mTimes --;
                    if (mCallBack != null){
                        mCallBack.onTick(mTimes);
                    }

                    delay -= mCountDownInterval;
                    if (mTimes <= 0){
                        finishCountDown();
                        return;
                    }
                }

                mHandler.postDelayed(this, 1000 - delay);
            }else {
                finishCountDown();
            }
        }
    };

    private void finishCountDown(){
        if (mCallBack != null){
            mCallBack.onFinish();
        }
        isStart = false;
    }

    public void cancel(){
        mHandler.removeCallbacksAndMessages(null);
        isStart = false;
    }

    public interface OnTimerCallBack{

        void onStart();

        void onTick(int times);

        void onFinish();

    }

}

思路就是在倒计时开始前获取一次SystemClock.elapsedRealtime(),没轮倒计时再获取一次SystemClock.elapsedRealtime()相减得到误差,根据delay校准。然后使用while循坏来处理跳秒的操作,与原生的CountDownTimer不同,这里如果跳了多少秒,就会返回多少次回调。

推荐阅读更多精彩内容