Android使用CountDownTimer實現驗證碼倒數計時

Lindroid發表於2017-09-28

等待總是讓人感到焦急和厭煩的,特別是看不到進展的等待。所以為了不讓使用者痴痴地等,我們在進行某些耗時操作時,一般都要設計一個進度條或者倒數計時器,讓進度視覺化,告訴使用者“等待之後更精彩”。在使用簡訊驗證碼註冊或者登入App就可以看到這樣的設計:點選“傳送驗證碼”的按鈕之後,按鈕上就會出現倒數計時(一般為60秒),倒數計時結束之後,按鈕的文字就會變成“重新傳送”。

在Android中要實現這樣的效果可以使用Handler傳送訊息,但其實還有一個已經封裝好的抽象類可以幫上忙,那就是CountDownTimer,利用它,我們可以很輕鬆地實現倒數計時。很久以前我就用過這個類,但是這幾天寫時發現了一個當初沒有注意到的坑,因此打算寫一篇部落格記錄下來。

1、需求分析

  1. 點選按鈕之後,按鈕文字變為“ns後傳送驗證碼”(n為倒數計時讀數);
  2. 為了讓倒數計時更加醒目,將秒數和單位設為藍色;
  3. 倒數計時結束之後,按鈕的文字顯示為“重新傳送”。

瞄一眼效果圖:

倒數計時效果圖
倒數計時效果圖

2、工程建立和佈局編寫

建立工程就不用多說了,由於我們只需要看到按鈕上的倒數計時效果,不必輸入手機號碼,所以只要在介面上簡單地放置一個按鈕即可:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    tools:context="com.lindroid.countdowndemo.MainActivity">

    <Button
        android:id="@+id/btn_captcha"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#c7c7c7"
        android:text="獲取驗證碼"
        android:textAllCaps="false"
        android:textColor="@android:color/black"
        android:textSize="18sp" />

</RelativeLayout>複製程式碼

3、如何使用CountDownTimer

CountDownTimer倒數計時器的使用並不難,我們可以建立一個類繼承它,並實現它的建構函式和重寫兩個方法:

    private CountTimer countTimer;

    /**
     * 點選按鈕後倒數計時
     */
    class CountTimer extends CountDownTimer {

        public CountTimer(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }

        /**
         * 倒數計時過程中呼叫
         *
         * @param millisUntilFinished
         */
        @Override
        public void onTick(long millisUntilFinished) {

        }

        /**
         * 倒數計時完成後呼叫
         */
        @Override
        public void onFinish() {

        }
    }複製程式碼

大體的框架如上所述,我來稍微解釋一下。首先是建構函式,裡面有兩個引數:

  • millisInFuture:倒數計時的總時間,單位為毫秒
  • countDownInterval:倒數計時的時間間隔,單位為毫秒
    比如我想設定10秒的倒數計時,每隔1秒就讀一次數,那麼初始化就可以將數值傳入:
    CountTimer countTimer = = new CountTimer(10000, 1000);複製程式碼
    除了建構函式,還有兩個方法,它們的作用分別如下:
  • onTick:倒數計時過程中呼叫
  • onFinish:倒數計時結束後呼叫

那麼怎麼開啟倒數計時呢?只需要用countTimer去呼叫start方法就可以了。另外,為了節省資源,在Activity銷燬時應該停止倒數計時:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        countTimer.cancel();
    }複製程式碼

到這裡,你應該知道怎麼使用如何使用CountDownTimer了吧?如果還有疑問,可以在文末下載完整的程式碼。

4、實現簡單的倒數計時效果

現在我們先來實現點選按鈕後就進行倒數計時讀數的效果,程式碼如下:

    CountTimer countTimer = new CountTimer(10000, 1000);

    /**
     * 點選按鈕後倒數計時
     */
    class CountTimer extends CountDownTimer {

        public CountTimer(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }

        /**
         * 倒數計時過程中呼叫
         *
         * @param millisUntilFinished
         */
        @Override
        public void onTick(long millisUntilFinished) {
            Log.e("Tag", "倒數計時=" + (millisUntilFinished/1000));
            btnCaptcha.setText(millisUntilFinished / 1000 + "s後重新傳送");
            //設定倒數計時中的按鈕外觀
            btnCaptcha.setClickable(false);//倒數計時過程中將按鈕設定為不可點選
            btnCaptcha.setBackgroundColor(Color.parseColor("#c7c7c7"));
            btnCaptcha.setTextColor(ContextCompat.getColor(context, android.R.color.black));
            btnCaptcha.setTextSize(16);
        }

        /**
         * 倒數計時完成後呼叫
         */
        @Override
        public void onFinish() {
            Log.e("Tag", "倒數計時完成");
            //設定倒數計時結束之後的按鈕樣式
            btnCaptcha.setBackgroundColor(ContextCompat.getColor(context, android.R.color.holo_blue_light));
            btnCaptcha.setTextColor(ContextCompat.getColor(context, android.R.color.white));
            btnCaptcha.setTextSize(18);
            btnCaptcha.setText("重新傳送");
            btnCaptcha.setClickable(true);
        }
    }複製程式碼

倒數計時的讀數是實時的,毫無疑問應該在onTick方法中處理這些邏輯,倒數計時完成後要將按鈕文字改為“重新傳送”,這個可以交給onFinish

執行一下,點選按鈕,倒數計時成功出現了,但是再點幾次,詭異的事情發生了:有時候倒數計時讀數會漏掉某個數字,比如從10直接就到8了,列印出來的日誌是這樣的:

跳秒現象
跳秒現象

這……到底是怎麼回事?少掉的一秒難道是被某人給續了麼?

5、CountDownTimer誤差解決

為了找回生命中的這一秒鐘,我在一個技術群裡和小夥伴們討論了很久,最後算是逃過了時間黑洞的魔爪。

我們採用的倒數計時讀數是將millisUntilFinished除於1000得到的,這裡就有一個小小的陷阱了:millisUntilFinished是長整型變數,除於1000之後得到是整數部分。我們可以將millisUntilFinished的值列印出來看看:

millisUntilFinished的值
millisUntilFinished的值

現在明白為什麼看不到讀數9了嗎?那是因為程式執行雖然很快,但再快也是需要時間的,所以從10秒倒數計時到9秒時,millisUntilFinished會比9000稍小一點,是8999,而長整型8999除於1000之後就得到8了。當然,既然是誤差那就有多種情況,少掉的數字不一定是9,這裡只是我針對我遇到的情況而言。

知道原因之後就好辦了,我們可以先將millisUntilFinished轉換成double型別後再除於1000,這樣就可以保留小數部分了,然後使用Math類中的round方法四捨五入,但是這樣倒數計時的話會從10到2,這顯然不行,所以再減去1,讓它從9到1。修改後的onTick方法程式碼是這樣的:

        public void onTick(long millisUntilFinished) {
            //處理後的倒數計時數值
            int time = (int) (Math.round((double) millisUntilFinished / 1000) - 1);
            btnCaptcha.setText(String.valueOf(time)+"s後重新傳送");
            //設定倒數計時中的按鈕外觀
            btnCaptcha.setClickable(false);//倒數計時過程中將按鈕設定為不可點選
            btnCaptcha.setBackgroundColor(Color.parseColor("#c7c7c7"));
            btnCaptcha.setTextColor(ContextCompat.getColor(context, android.R.color.black));
            btnCaptcha.setTextSize(16);
        }複製程式碼

執行後試試,就可以發現失去的那一秒又回來啦。

6、給倒數計時讀數和單位設定前景色

給同一字串中的不同字元設定不同的字型顏色,這就需要用到SpannableString與SpannableStringBuilder相關的知識了,限於篇幅,這裡就不贅述了,可以參考這篇文章:SpannableString與SpannableStringBuilder。這裡只簡單介紹一下:

6.1 拼接字串

            int time = (int) (Math.round((double) millisUntilFinished / 1000) - 1);
            //拼接要顯示的字串
            SpannableStringBuilder sb = new SpannableStringBuilder();
            sb.append(String.valueOf(time));
            sb.append("s後重新傳送");複製程式碼

6.2 設定要顯示的文字樣式

            //字元“後”在字串中的下標
            int index = String.valueOf(sb).indexOf("後");
            //給秒數和單位設定藍色前景色
            ForegroundColorSpan colorSpan = new ForegroundColorSpan(ContextCompat.getColor(context, android.R.color.holo_blue_dark));
            sb.setSpan(colorSpan, 0, index, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
            btnCaptcha.setText(sb);複製程式碼

這次執行之後就可以看到跟效果圖一樣的效果了。最後給一下原始碼:
CountDownTimerDemo

參考文章

SpannableString與SpannableStringBuilder
谷歌文件之CountDownTimer

相關文章