cekiasoo's blog Android Coder

Android 倒计时 CountDownTimer 的使用和封装及改进

2017-03-05
cekiasoo

阅读:


(一)介绍

官方文档。CountDownTimer是一个倒计时的类,还可以指定时间间隔定期通知,举个栗子,比如说你倒计时是100秒的,可以指定每20秒通知一次,这样开始的时候会回调一次,20秒时会回调一次,40秒时会回调一次…,200秒时的回调和时间间隔的回调不同方法的。

(二)使用

CountDownTimer只有一个构造,

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

从方法的注释中可以看出,第一个参数是总的倒计时时间,第二个参数是定期的回调时间。
使用的时候:

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

onTick()方法是定期间隔回调的方法,onFinish()就是结束时回调的方法了。
运行结果截图
可以看出在9970毫秒的时候回调了一次onTick()方法,因为消息传递会消耗一点时间,使用传过来时是9976毫秒,后面的也是,这里执行了onTick()方法4次,那剩下2秒的时候不应该还要执行一次么?怎么3964后就不执行onTick()方法了?原因是3964毫秒再经过2秒就剩下1900多毫秒,比2秒小,使用不执行onTick()方法。
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);
}

如果倒计时后不让它执行了也可以取消倒计时,调用CountDownTimer的cancel()方法就可以取消倒计时了。

(三)封装

有时候我们并不关心onTick(),只需要关心onFinish()方法的执行,我们可以封装个工具类,把这两个功能分开,想单独用时就单独用,想合起来用时就合起来用。

package com.ce.countdowntimertest.utils;
import android.os.CountDownTimer;
/**
 * 倒计时工具类
 */
public class CountDownTimerUtils {
    /**
     * 倒计时结束的回调接口
     */
    public interface FinishDelegate {
        void onFinish();
    }
    /**
     * 定期回调的接口
     */
    public interface TickDelegate {
        void onTick(long pMillisUntilFinished);
    }
    private final static long ONE_SECOND = 1000;
    /**
     * 总倒计时时间
     */
    private long mMillisInFuture = 0;
    /**
     * 定期回调的时间 必须大于0 否则会出现ANR
     */
    private long mCountDownInterval;
    /**
     * 倒计时结束的回调
     */
    private FinishDelegate mFinishDelegate;
    /**
     * 定期回调
     */
    private TickDelegate mTickDelegate;
    private MyCountDownTimer mCountDownTimer;
    /**
     * 获取 CountDownTimerUtils
     * @return CountDownTimerUtils
     */
    public static CountDownTimerUtils getCountDownTimer() {
        return new CountDownTimerUtils();
    }
    /**
     * 设置定期回调的时间 调用{@link #setTickDelegate(TickDelegate)}
     * @param pCountDownInterval 定期回调的时间 必须大于0
     * @return CountDownTimerUtils
     */
    public CountDownTimerUtils setCountDownInterval(long pCountDownInterval) {
        this.mCountDownInterval=pCountDownInterval;
        return this;
    }
    /**
     * 设置倒计时结束的回调
     * @param pFinishDelegate 倒计时结束的回调接口
     * @return CountDownTimerUtils
     */
    public CountDownTimerUtils setFinishDelegate(FinishDelegate pFinishDelegate) {
        this.mFinishDelegate=pFinishDelegate;
        return this;
    }
    /**
     * 设置总倒计时时间
     * @param pMillisInFuture 总倒计时时间
     * @return CountDownTimerUtils
     */
    public CountDownTimerUtils setMillisInFuture(long pMillisInFuture) {
        this.mMillisInFuture=pMillisInFuture;
        return this;
    }
    /**
     * 设置定期回调
     * @param pTickDelegate 定期回调接口
     * @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);
    }
    /**
     * 开始倒计时
     */
    public void start() {
        if (mCountDownTimer == null) {
            create();
        }
        mCountDownTimer.start();
    }
    /**
     * 取消倒计时
     */
    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;
        }
    }
}

使用也简单,用getCountDownTimer()方法获取实例;setMillisInFuture()方法是设置总倒计时时间;setFinishDelegate()方法是设置倒计时完成的回调;setCountDownInterval()方法是设置定期回调的时间间隔,但值要大于0,不然会出现ANR的,因为不停的回调会导致Looper消息处理不过来;setTickDelegate()方法是设置定期回调的。 只需要最终倒计时的使用:

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

运行结果截图
也可以只要定期的间隔回调,不过要设置设置下总倒计时时间:

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

运行结果截图
当然也可以两个都有:

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

运行结果截图

(四)改进

虽然CountDownTimer很好用,但有个缺点,就是只能在主线程中运行,如果在子线程中运行就会报错,

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

运行结果截图
原因是在主线程中Handler的Looper Android系统已经在framework层帮我们prepare(prepareMainLooper())过了,但在我们这个子线程没有Looper,那怎么办?我们可以借助HandlerThread完成,新建个自己的CountDownTimer类,把Android的CountDownTimer的源代码复制到我们新建的CountDownTimer类中,修改下包名,就不要用Android的CountDownTimer这个类的了,改造如下:

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

如果是主线程就不用管了,如果是子线程我们就把HandlerThread的getLooper()拿出来给Handler。这里我用的是Handler.Callback来进行对消息处理的。现在我们把刚才在子线程中运行的CountDownTimer类改成我们自己定义的CountDownTimer类在运行一下:

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

运行结果截图
可以运行了,当然在主线程也是正常的:

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

运行结果截图
如果在CountDownTimerUtils类里想用这个改进的CountDownTimer,改下CountDownTimer的包名就可以运行。


Similar Posts

Comments