1. 程式人生 > >淺談android中僅僅使用一個TextView實現高仿京東,淘寶各種倒計時

淺談android中僅僅使用一個TextView實現高仿京東,淘寶各種倒計時

  今天給大家帶來的是僅僅使用一個TextView實現一個高仿京東、淘寶、唯品會等各種電商APP的活動倒計時。最近公司一直加班也沒來得及時間去整理,今天難得休息想把這個分享給大家,只求共同學習,以及自己後續的複習。為什麼會想到使用一個TextView來實現呢?因為最近公司在做一些優化的工作,其中就有一個倒計時樣式,原來開發的這個控制元件的同事使用了多個TextView拼接在一起的,實現的程式碼冗餘比較大,故此專案經理就說:小巨集這個就交給你來優化了,並且還要保證有一定的擴充套件性,當時就懵逼了。不知道從何處開始優化。然後我就檢視京東,餓了麼,唯品會等各個APP的倒計時,並在開發者中開啟層級介面顯示,發現他們都有一個共同的特點就是一個View,沒有使用多個TextView來拼接。相信大家都知道僅僅使用一個TextView比使用多個TextView拼接去實現的優勢吧,下面不妨來看看幾個介面就知道了。



看到這個,大家心裡自然就想到了自定義View來實現吧。對,自定義View確實可以實現這樣的效果。但是今天我們不採用自定義View來做。而是使用一個TextView來實現。

由於專案經理要求此次優化的程式碼具有可擴充套件性。所以此次程式碼的設計加了一些面向物件的知識。有一些自己的設計和架構的思路。

此次demo的設計思路:

          1、編寫一個倒計時的基類作為實現最普通和最基本的倒計時的功能,沒有任何樣式,讓這個基類去繼承CountDownTimer類,並且在該基類中

儲存一個TextView的物件,並且把每次倒計時的資料,顯示在TextView中,然後公佈一個getmDateTv()方法返回一個TextView物件即可。然後只要拿到這個TextView物件顯示介面的佈局中即可。非常方便。

          2、然後不同樣式的倒計時,只需要編寫不同的子類去繼承最普通的倒計時基類即可,然後重寫其中的設定資料和設定樣式的兩個方法即可,然後就能給最普通的倒計時新增不同的樣式。下次如果需要擴充套件新的倒計時樣式,不需要改變其他類的程式碼,只需編寫一個普通倒計時的派生類重寫兩個方法即可,使得可擴充套件性更靈活。

          3、然後通過一個TimerUtils管理類,去集中承擔子類和父類壓力,讓子類和父類所需實現功能分擔到TimerUtils類中,並且該TimerUtils管理類是與客戶端唯一打交道的類,比如獲得倒計時物件以及獲得倒計時的TextView物件都通過這個管理類分配,避免客戶端直接與倒計時的基類和子類打交道。從而使得類的封裝性和隱藏性得到體現。

下面可以看下這個Demo設計的簡單的UML類圖:


通過以上思路分析下面我們就看看此次Demo的實現需要用到哪些知識點.

            1、CountDownTimer類的用法。

    2、SpannableString的用法。

    3、MikyouCountDownTimer的封裝。

    4、自定義MikyouBackgroundSpan的實現。

一、通過以上的分析我們首先得複習一下有關CountDownTimer的知識,CountDownTimer是一個很簡單的類我們可以看下的它的原始碼,它的用法自然就知道了。

CountDownTimer是一個抽象類。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package android.os;

public abstract class CountDownTimer {
    public CountDownTimer(long millisInFuture, long countDownInterval) {
        throw new RuntimeException("Stub!");
    }

    public final synchronized void cancel() {
        throw new RuntimeException("Stub!");
    }

    public final synchronized CountDownTimer start() {
        throw new RuntimeException("Stub!");
    }

    public abstract void onTick(long var1);

    public abstract void onFinish();
}

可以看到倒計時的總時長就是millisFuture,和countDownInterVal間隔步長預設是1000ms,所以資料都是通過其構造器進行初始化,然後需要去重寫一個回撥方法onTick,

裡面的一個引數就是每隔相應的步長後剩餘的時間毫秒數。然後我們只需要在onTick方法中將每隔1000ms時間毫秒數進行時間格式化即可得到相應時間格式的倒計時這就是實現了最基本倒計時樣式。格式化倒計時格式採用的是apache中的common的lang包中DurationFormatUtils類中的formatDuration,通過傳入一個時間格式就會自動將倒計時轉換成相應的mTimePattern的樣式(HH:mm:ss或dd天HH時mm分ss秒).

二、複習一下有關SpannableString的用法。

在Android中EditText用於編輯文字,TextView用於顯示文字,但是有時候我們需要對其中的文字進行樣式等方面的設定。Android為我們提供了SpannableString類來對指定文字進行處理。

1) ForegroundColorSpan        文字顏色

private void setForegroundColorSpan() {    
    SpannableString spanString = new SpannableString("前景色");    
    ForegroundColorSpan span = new ForegroundColorSpan(Color.BLUE);    
    spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
    tv.append(spanString);    
}    


2) BackgroundColorSpan 文字背景色 

private void setBackgroundColorSpan() {    
    SpannableString spanString = new SpannableString("背景色");    
    BackgroundColorSpan span = new BackgroundColorSpan(Color.YELLOW);    
    spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
    tv.append(spanString);    
}   


3) StyleSpan         字型樣式:粗體、斜體等


private void setStyleSpan() {    
    SpannableString spanString = new SpannableString("粗體斜體");    
    StyleSpan span = new StyleSpan(Typeface.BOLD_ITALIC);    
    spanString.setSpan(span, 0, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
    tv.append(spanString);    
}    


4) RelativeSizeSpan 相對大小


private void setRelativeFontSpan() {  
    SpannableString spanString = new SpannableString("字型相對大小");  
    spanString.setSpan(new RelativeSizeSpan(2.5f), 0, 6,Spannable.SPAN_INCLUSIVE_EXCLUSIVE);  
    tv.append(spanString);      
}  


5) TypefaceSpan         文字字型


private void setTypefaceSpan() {  
    SpannableString spanString = new SpannableString("文字字型");  
    spanString.setSpan(new TypefaceSpan("monospace"), 0, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);  
    tv.append(spanText);  
}  


6) URLSpan 文字超連結

private void addUrlSpan() {    
    SpannableString spanString = new SpannableString("超連結");    
    URLSpan span = new URLSpan("http://www.baidu.com");    
    spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
    tv.append(spanString);    
}    


7) ImageSpan         圖片

private void addImageSpan() {    
    SpannableString spanString = new SpannableString(" ");    
    Drawable d = getResources().getDrawable(R.drawable.ic_launcher);    
    d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());    
    ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BASELINE);    
    spanString.setSpan(span, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
    tv.append(spanString);    
}   


8) ClickableSpan                 文字有點選事件

private TextView textView;  
textView = (TextView)this.findViewById(R.id.textView);  
String text = "顯示Activity";  
SpannableString spannableString = new SpannableString(text);  
spannableString.setSpan(new ClickableSpan() {  
    @Override  
    public void onClick(View widget) {  
        Intent intent = new Intent(Main.this,OtherActivity.class);  
        startActivity(intent);  
    }  
    // 表示點選整個text的長度都有效觸發這個事件  
}, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);  
textView.setText(spannableString);  
textView.setMovementMethod(LinkMovementMethod.getInstance());  


9) UnderlineSpan         下劃線

private void addUnderLineSpan() {    
    SpannableString spanString = new SpannableString("下劃線");    
    UnderlineSpan span = new UnderlineSpan();    
    spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
    tv.append(spanString);    
}   


10) StrikethroughSpan 
        刪除線

private void addStrikeSpan() {    
    SpannableString spanString = new SpannableString("刪除線");    
    StrikethroughSpan span = new StrikethroughSpan();    
    spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    
    tv.append(spanString);    
}    


11) SuggestionSpan 
相當於佔位符


12) MaskFilterSpan 
修飾效果,如模糊(BlurMaskFilter)、浮雕(EmbossMaskFilter)


13) RasterizerSpan 
        光柵效果


14) AbsoluteSizeSpan 
        絕對大小(文字字型)

private void setAbsoluteFontSpan() {  
        SpannableString spannableString = new SpannableString("40號字型");  
        AbsoluteSizeSpan absoluteSizeSpan = new AbsoluteSizeSpan(40);  
        spannableString.setSpan(absoluteSizeSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);  
        editText.append(spannableString);  
}  


15) DynamicDrawableSpan    設定圖片,基於文字基線或底部對齊。


16) TextAppearanceSpan 
文字外貌(包括字型、大小、樣式和顏色)

private void setTextAppearanceSpan() {  
    SpannableString spanString = new SpannableString("文字外貌");  
    TextAppearanceSpan textAppearanceSpan = new TextAppearanceSpan(this, android.R.style.TextAppearance_Medium);  
    spanString.setSpan(textAppearanceSpan, 0, 4, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);  
    tv.append(spanString);  
}  

好了,通過以上的複習知識點,現在我們就可以來真正開始demo的實現,然後我們一起來一步一步封裝我們的倒計時。

一、編寫一個MikyouCountDownTimer基類,讓它去繼承CountDownTimer類,並且公佈出initSpanData和setBackgroundSpan方法用於其他樣式倒計時的子類使用,它可以實現最基本倒計時的功能。

package com.mikyou.countdowntimer.bean;
import android.content.Context;
import android.os.CountDownTimer;
import android.text.style.ForegroundColorSpan;
import android.widget.TextView;
import com.mikyou.countdowntimer.myview.MikyouBackgroundSpan;
import com.mikyou.countdowntimer.utils.TimerUtils;
import org.apache.commons.lang.time.DurationFormatUtils;
import java.util.ArrayList;
import java.util.List;
/**
 * Created by mikyou on 16-10-22.
 */
public class MikyouCountDownTimer extends CountDownTimer{
    private Context mContext;//傳入的上下文物件
    protected TextView mDateTv;//一個TextView實現倒計時
    private long  mGapTime;//傳入設定的時間間隔即倒計時的總時長
    private long mCount = 1000;//倒計時的步長 一般為1000代表每隔1s跳一次
    private String mTimePattern = "HH:mm:ss";//timePattern 傳入的時間的樣式 如: HH:mm:ss HH時mm分ss秒 dd天HH時mm分ss秒
    private String mTimeStr;
    protected List<MikyouBackgroundSpan> mBackSpanList;
    protected List<ForegroundColorSpan> mTextColorSpanList;
    private int mDrawableId;
    private boolean flag = false;//設定標記flag,用於控制使得初始化Span的資料一次

    protected  String[] numbers;//此陣列用於儲存每個倒計時字元拆分後的天,時,分,秒的數值
    protected char[]  nonNumbers;//儲存了天,時,分,秒之間的間隔("天","時","分","秒"或者":")
   //用於倒計時樣式的內間距,字型大小,字型顏色,倒計時間隔的顏色
    private int mSpanPaddingLeft,mSpanPaddingRight,mSpanPaddingTop,mSpanPaddingBottom;
    private int mSpanTextSize;
    private int mSpanTextColor;
    protected int mGapSpanColor;
    public MikyouCountDownTimer(Context mContext, long mGapTime, String mTimePattern,int mDrawableId) {
        this(mContext,mGapTime,1000,mTimePattern,mDrawableId);
    }
    public MikyouCountDownTimer(Context mContext, long mGapTime, int mCount, String mTimePattern,int mDrawableId) {
        super(mGapTime,mCount);
        this.mContext = mContext;
        this.mGapTime = mGapTime;//倒計時總時長
        this.mCount = mCount;//每次倒計時的步長,預設是1000
        this.mDrawableId= mDrawableId;//用於設定背景的drawable的id
        this.mTimePattern = mTimePattern;//時間的格式:如HH:mm:ss或者dd天HH時mm分ss秒等
        mBackSpanList = new ArrayList<>();
        mTextColorSpanList = new ArrayList<>();
        mDateTv = new TextView(mContext,null);
    }
    //公佈這些設定倒計時樣式的方法,供外部呼叫,從而靈活定製倒計時的樣式
    public MikyouCountDownTimer setTimerTextSize(int textSize){
        this.mSpanTextSize = textSize;
        return this;
    }
    public MikyouCountDownTimer setTimerPadding(int left,int top,int right,int bottom){
        this.mSpanPaddingLeft = left;
        this.mSpanPaddingBottom = bottom;
        this.mSpanPaddingRight = right;
        this.mSpanPaddingTop = top;
        return this;
    }
    public MikyouCountDownTimer setTimerTextColor(int color){
        this.mSpanTextColor = color;
        return this;
    }
    public MikyouCountDownTimer setTimerGapColor(int color){
        this.mGapSpanColor = color;
        return this;
    }
    //設定倒計時的Span的樣式,公佈出給各個子類實現
    public void setBackgroundSpan(String timeStr) {
        if (!flag){
            initSpanData(timeStr);
            flag = true;
        }
        mDateTv.setText(timeStr);
    }
    //設定倒計時的Span的資料,公佈出給各個子類實現
    public void initSpanData(String timeStr) {
        numbers = TimerUtils.getNumInTimerStr(timeStr);
        nonNumbers = TimerUtils.getNonNumInTimerStr(timeStr);
    }

    protected void initBackSpanStyle(MikyouBackgroundSpan mBackSpan) {
        mBackSpan.setTimerPadding(mSpanPaddingLeft,mSpanPaddingTop,mSpanPaddingRight,mSpanPaddingBottom);
        mBackSpan.setTimerTextColor(mSpanTextColor);
        mBackSpan.setTimerTextSize(mSpanTextSize);
    }

    @Override
    public void onTick(long l) {
        if (l > 0) {
            mTimeStr = DurationFormatUtils.formatDuration(l, mTimePattern);
            //這是apache中的common的lang包中DurationFormatUtils類中的formatDuration,通過傳入
            //一個時間格式就會自動將倒計時轉換成相應的mTimePattern的樣式(HH:mm:ss或dd天HH時mm分ss秒)
            setBackgroundSpan(mTimeStr);
        }
    }

    @Override
    public void onFinish() {
        mDateTv.setText("倒計時結束");
    }
    //用於返回顯示倒計時的TextView的物件
    public TextView getmDateTv() {
        startTimer();
        return mDateTv;
    }
    public void cancelTimer(){
        this.cancel();
    }
    public void startTimer(){
        this.start();
    }

    public String getmTimeStr() {
        return mTimeStr;
    }
}
TimerUtils類用於儲存不同倒計時的格式,例如HH:mm:ss、HH時mm分ss秒、dd天HH時mm分ss秒等。現在我們可以來看下簡單的基本樣式。


二、自定義MikyouBackgroundSpan去繼承ImageSpan,這個類非常重要是用於給倒計時的TextView加樣式,為什麼可以使用一個TextView來實現呢
別忘了還有個很強悍的類就是SpannableString類,這個類就是可以設定一段字串中的每個字元的樣式,很多樣式。最後通過TextView中有個setSpan方法即可傳入
一個SpannableString物件完成設定。但是為什麼需要自定義一個Span呢?這是因為很奇怪為什麼android中的那麼多Span樣式中沒有一個可以直接設定一個drawable物件檔案呢,所以上網找了很多都沒有找到,最後在stackOverFlow上找到了一個外國人給了一個解決辦法,就是重寫ImageSpan最後就可以實現了設定drawable檔案即可

package com.mikyou.countdowntimer.myview;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.style.ImageSpan;

/**
 * Created by mikyou on 16-10-22.
 */
public class MikyouBackgroundSpan extends ImageSpan {
    private Rect mTextBound;
    private int maxHeight = 0;
    private int maxWidth = 0;
    private int mPaddingLeft = 20;
    private int mPaddingRight = 20;
    private int mPaddingTop =  20;
    private int mPaddingBottom = 20;
    private int mTextColor = Color.GREEN;
    private int mTextSize = 50;
    public MikyouBackgroundSpan(Drawable d, int verticalAlignment) {
        super(d, verticalAlignment);
        mTextBound = new Rect();
    }

    public MikyouBackgroundSpan setTimerTextColor(int mTextColor) {
        this.mTextColor = mTextColor;
        return this;
    }
   public MikyouBackgroundSpan setTimerTextSize(int textSize){
       this.mTextSize = textSize;
       return this;
   }
    public MikyouBackgroundSpan setTimerPadding(int left,int top,int right,int bottom){
        this.mPaddingLeft = left;
        this.mPaddingRight = right;
        this.mPaddingBottom = bottom;
        this.mPaddingTop = top;
        return this;
    }
    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        //繪製文字的內容的背景
        paint.setTextSize(mTextSize);
        //測量文字的寬度和高度,通過mTextBound得到
        paint.getTextBounds(text.toString(), start, end, mTextBound);
        //設定文字背景的寬度和高度,傳入的是left,top,right,bottom四個引數
        maxWidth = maxWidth < mTextBound.width() ? mTextBound.width() : maxWidth;
        maxHeight = maxHeight < mTextBound.height() ? mTextBound.height() : maxHeight;
        //設定最大寬度和最大高度是為了防止在倒計時在數字切換的過程中會重繪,會導致倒計時邊框的寬度和高度會抖動,
        // 所以每次取得最大的高度和寬度而不是每次都去取測量的高度和寬度
        getDrawable().setBounds(0,0, maxWidth+mPaddingLeft+mPaddingRight,mPaddingTop+mPaddingBottom+maxHeight);
        //繪製文字背景
        super.draw(canvas, text, start, end, x, top, y, bottom, paint);
        //設定文字的顏色
        paint.setColor(mTextColor);
        //設定字型的大小
        paint.setTextSize(mTextSize);
        int mGapX = (getDrawable().getBounds().width() - maxWidth)/2;
        int mGapY= (getDrawable().getBounds().height() - maxHeight)/2;
        //繪製文字內容
        canvas.drawText(text.subSequence(start, end).toString(), x + mGapX , y - mGapY + maxHeight/3, paint);    }
}

三、樣式一的倒計時實現,樣式一指的是例如:12時36分27秒或者12:36:27就是將數值和時、分、秒或者":"分隔開,然後去自定義每塊數值(12  36 27)和間隔(時 分 秒 或 :)的樣式,包括給數值塊加背景和邊框。在MikyouCountDownTimer中的number陣列中儲存著[12 36 27]而nonumer陣列中儲存著[時 分 秒 ]或[ : :]d的間隔字元。
package com.mikyou.countdowntimer.bean;

import android.content.Context;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;

import com.mikyou.countdowntimer.myview.MikyouBackgroundSpan;
import com.mikyou.countdowntimer.utils.TimerUtils;

/**
 * Created by mikyou on 16-10-22.
 */
public class JDCountDownTimer extends MikyouCountDownTimer {
    private SpannableString mSpan;
    private Context mContext;
    private int mDrawableId;
    public JDCountDownTimer(Context mContext, long mGapTime, String mTimePattern,int mDrawableId) {
        super(mContext, mGapTime, mTimePattern,mDrawableId);
        this.mContext = mContext;
        this.mDrawableId = mDrawableId;
    }
  /**
   * 重寫父類的initSpanData方法
   * 通過number陣列得到每塊數值對應的自定義MikyouBackgroundSpan物件
   * 然後通過MikyouBackgroundSpan物件定義每塊數值的樣式包括背景,邊框,邊框圓角樣式,然後將這些物件加入到集合中去
   * 通過nonNumber陣列得到每個間隔的ForegroundColorSpan物件
   * 然後通過這些物件就可以定義每個間隔塊的樣式,因為只定義了ForegroundColorSpan所以只能定義
   * 每個間隔塊的字型顏色,setmGapSpanColor方式也是供外部自由定製每個間隔的樣式
   * 實際上還可以定義其他的Span,同理實現也是很簡單的。
   * */
    @Override
    public void initSpanData(String timeStr) {
        super.initSpanData(timeStr);
        for (int i = 0; i<numbers.length;i++){
            MikyouBackgroundSpan mBackSpan = new MikyouBackgroundSpan(mContext.getDrawable(mDrawableId), ImageSpan.ALIGN_BOTTOM);
            initBackSpanStyle(mBackSpan);
            mBackSpanList.add(mBackSpan);
        }
        for (int i= 0; i<nonNumbers.length;i++){
            ForegroundColorSpan mGapSpan = new ForegroundColorSpan(mGapSpanColor);
            mTextColorSpanList.add(mGapSpan);
        }
    }

    /** 重寫父類的setBackgroundSpan方法
     * 我們知道設定Span的樣式主要是控制兩個變數start,end索引
     * 以確定設定start到end位置的字串的子串的樣式
     * mGapLen = 1,表示一個間隔塊的長度,
     * 例如:12時36分27秒的"時","分","秒"的間隔長度
     * 所以通過遍歷Span集合,給字串設定Span,
     * 通過分析不難得出每個數值塊的Span的start索引:start = i*numbers[i].length() + i*mGapLen;
     * end = start + numbers[i].length();
     * */
    @Override
    public void setBackgroundSpan(String timeStr) {
        super.setBackgroundSpan(timeStr);
        int mGapLen = 1;
        mSpan = new SpannableString(timeStr);
        for (int i = 0;i<mBackSpanList.size();i++){
            int start = i*numbers[i].length() + i*mGapLen;
            int end = start + numbers[i].length();
            TimerUtils.setContentSpan(mSpan,mBackSpanList.get(i),start,end);
            
            if (i < mTextColorSpanList.size()){//這裡為了就是防止12:36:27這種樣式,這種樣式間隔只有2個所以需要做判斷,防止陣列越界
                TimerUtils.setContentSpan(mSpan,mTextColorSpanList.get(i),end,end + mGapLen);
            }
        }
        mDateTv.setMovementMethod(LinkMovementMethod.getInstance());//此方法很重要需要呼叫,否則繪製出來的倒計時就是重疊的樣式
        mDateTv.setText(mSpan);
    }

}

四、樣式二的倒計時實現,樣式二不同於樣式一的是例如:12時36分27秒或者12:36:27就是將每個數值和時、分、秒或者":"分隔開,然後去自定義每塊數值(1 2 3 6 2 7)和間隔(時 分 秒 或 :)的樣式,包括給數值塊加背景和邊框。在MikyouCountDownTimer中的vipNumber陣列中儲存著[1 2 3 6 2 7]而vipnonNumer陣列中儲存著[時 分 秒 ]或[ : :]d的間隔字元。
package com.mikyou.countdowntimer.bean;

import android.content.Context;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;

import com.mikyou.countdowntimer.myview.MikyouBackgroundSpan;
import com.mikyou.countdowntimer.utils.TimerUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by mikyou on 16-10-22.
 */
public class VIPCountDownTimer extends MikyouCountDownTimer {
    private SpannableString mSpan;
    private Context mContext;
    private int mDrawableId;
    private List<MikyouBackgroundSpan> mSpanList;
    private String[] vipNumbers;
    private char[] vipNonNumbers;
    public VIPCountDownTimer(Context mContext, long mGapTime, String mTimePattern,int mDrawableId) {
        super(mContext, mGapTime, mTimePattern,mDrawableId);
        this.mContext = mContext;
        this.mDrawableId = mDrawableId;
        mSpanList = new ArrayList<>();
    }
    /** 重寫父類的setBackgroundSpan方法
     * 我們知道設定Span的樣式主要是控制兩個變數start,end索引
     * 以確定設定start到end位置的字串的子串的樣式,表示每個數字子串在整個字串中的位置範圍
     * mGapLen = 1,表示一個間隔塊的長度,
     * 例如:12時36分27秒的"時","分","秒"的間隔長度
     * 所以通過遍歷Span集合,給字串設定Span,
     * 通過分析不難得出每個數值塊的Span的start索引:start = i*numbers[i].length() + i*mGapLen;
     * end = start + numbers[i].length();
     * */
    @Override
    public void setBackgroundSpan(String timeStr) {
        int mGapLen = 1;
        mSpan = new SpannableString(timeStr);
        initSpanData(timeStr);
        int start = 0 ;
        int count =0;
        for (int i=0;i<vipNumbers.length;i++){

            for (int j=start;j<start + vipNumbers[i].toCharArray().length;j++,count++){
                TimerUtils.setContentSpan(mSpan,mSpanList.get(count),j,j+mGapLen);
            }
            //此時表示遍歷完了某一塊的數值,從而需要將此時該塊數值去更新start變數
            start = start + vipNumbers[i].toCharArray().length;
            if (i < nonNumbers.length){
                TimerUtils.setContentSpan(mSpan,mTextColorSpanList.get(i),start,start+mGapLen);
                start = start +mGapLen;//如果是個間隔還得去加上每個間隔長度最後去更新start變數
            }

        }
        mDateTv.setMovementMethod(LinkMovementMethod.getInstance());
        mDateTv.setText(mSpan);
    }
    /**
     * 重寫父類的initSpanData方法
     * 通過number陣列得到每塊數值對應的自定義MikyouBackgroundSpan物件
     * 然後通過MikyouBackgroundSpan物件定義每塊數值的樣式包括背景,邊框,邊框圓角樣式,然後將這些物件加入到集合中去
     * 通過nonNumber陣列得到每個間隔的ForegroundColorSpan物件
     * 然後通過這些物件就可以定義每個間隔塊的樣式,因為只定義了ForegroundColorSpan所以只能定義
     * 每個間隔塊的字型顏色,setmGapSpanColor方式也是供外部自由定製每個間隔的樣式
     * 實際上還可以定義其他的Span,同理實現也是很簡單的。
     * */
    @Override
    public void initSpanData(String timeStr) {
        super.initSpanData(timeStr);
        vipNumbers = TimerUtils.getNumInTimerStr(timeStr);//得到每個數字注意不是每塊數值,並加入陣列
        vipNonNumbers = TimerUtils.getNonNumInTimerStr(timeStr);//得到每個間隔字元,並加入到陣列
        for (int i=0;i<vipNumbers.length;i++){
            for (int j=0;j<vipNumbers[i].toCharArray().length;j++){//因為需要得到每個數字所以還得遍歷每塊數值中的每個數字,所以需要二層迴圈
                MikyouBackgroundSpan mSpan = new MikyouBackgroundSpan(mContext.getDrawable(mDrawableId), ImageSpan.ALIGN_BOTTOM);
                initBackSpanStyle(mSpan);
                mSpanList.add(mSpan);
            }
        }
        for (int i= 0; i<vipNonNumbers.length;i++){
            ForegroundColorSpan mGapSpan = new ForegroundColorSpan(mGapSpanColor);
            mTextColorSpanList.add(mGapSpan);
        }
    }
}
四、TimerUtils管理類,主要是提供不同樣式的倒計時的物件給客戶端,所以這個類直接與客戶端建立關係,從而實現倒計時子類和基類對外界的隱藏體現了封裝性。
package com.mikyou.countdowntimer.utils;

import android.content.Context;
import android.graphics.Color;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;

import com.mikyou.countdowntimer.bean.JDCountDownTimer;
import com.mikyou.countdowntimer.bean.MikyouCountDownTimer;
import com.mikyou.countdowntimer.bean.VIPCountDownTimer;

/**
 * Created by mikyou on 16-10-22.
 */
public class TimerUtils {
    public static final int JD_STYLE = 0;
    public static final int VIP_STYLE = 1;
    public static final int DEFAULT_STYLE = 3;

    public static final String TIME_STYLE_ONE = "HH:mm:ss";
    public static final String TIME_STYLE_TWO = "HH時mm分ss秒";
    public static final String TIME_STYLE_THREE = "dd天HH時mm分ss秒";
    public static final String TIME_STYLE_FOUR = "dd天HH時mm分";

    public static MikyouCountDownTimer getTimer(int style,Context mContext, long mGapTime, String mTimePattern, int mDrawableId){
        MikyouCountDownTimer mCountDownTimer = null;
        switch (style){
            case JD_STYLE:
                mCountDownTimer = new JDCountDownTimer(mContext,mGapTime,mTimePattern,mDrawableId);
                break;
            case VIP_STYLE:
                mCountDownTimer = new VIPCountDownTimer(mContext,mGapTime,mTimePattern,mDrawableId);
                break;
            case DEFAULT_STYLE:
                mCountDownTimer = new MikyouCountDownTimer(mContext,mGapTime,mTimePattern,mDrawableId);
                break;
        }
        return mCountDownTimer;
    }
//得到倒計時字串中的數值塊部分
    public static String[] getNumInTimerStr(String mTimerStr){
        return mTimerStr.split("[^\\d]");
    }
    //得到倒計時中字串中的非數值的字串,並把數值過濾掉重新組合成一個字串,並把字串拆分字元陣列,也就是儲存倒計時中間的間隔
    public static char[] getNonNumInTimerStr(String mTimerStr){
        return mTimerStr.replaceAll("\\d","").toCharArray();
    }
   //設定字型顏色
    public static ForegroundColorSpan getTextColorSpan(String color){
        ForegroundColorSpan mSpan = null;
        if (mSpan == null){
            mSpan = new ForegroundColorSpan(Color.parseColor(color));
        }
        return mSpan;
    }
    //設定內容的Span
    public static void setContentSpan(SpannableString mSpan, Object span, int start,
                               int end) {
        mSpan.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

}
現在我們就來測試下我們使用一個TextView實現的倒計時。

使用該倒計時非常簡單非常方便只需要一行程式碼就能實現一個高仿京東和各種電商的APP的倒計時樣式。

package com.mikyou.countdowntimer;

import android.graphics.Color;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Gravity;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.mikyou.countdowntimer.utils.TimerUtils;

public class MainActivity extends AppCompatActivity {
    private LinearLayout parent;
    private int padding =10;
    private int textSize = 40;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        parent = (LinearLayout) findViewById(R.id.parent);
        //預設樣式倒計時每種樣式下又對應四種時間的格式
        /**
         * 預設+時間格式1:DEFAULT_STYLE <--> TIME_STYLE_ONE = "HH:mm:ss"
         * */
        TextView tv = TimerUtils.getTimer(TimerUtils.DEFAULT_STYLE,this,120000000,TimerUtils.TIME_STYLE_ONE,0)
                .getmDateTv();
        parent.addView(tv);
        setmLayoutParams(tv);
        /**
         * 預設+時間格式2:DEFAULT_STYLE <--> TIME_STYLE_TWO = "HH時mm分ss秒"
         * */
        TextView tv1 = TimerUtils.getTimer(TimerUtils.DEFAULT_STYLE,this,120000000,TimerUtils.TIME_STYLE_TWO,0)
                .getmDateTv();
        parent.addView(tv1);
        setmLayoutParams(tv1);
        /**
         * 預設+時間格式3:DEFAULT_STYLE <--> TIME_STYLE_THREE = "dd天HH時mm分ss秒"
         * */
        TextView tv2 = TimerUtils.getTimer(TimerUtils.DEFAULT_STYLE,this,120000000,TimerUtils.TIME_STYLE_THREE,0)
                .getmDateTv();
        parent.addView(tv2);
        setmLayoutParams(tv2);
        /**
         * 預設+時間格式4:DEFAULT_STYLE <--> TIME_STYLE_FOUR = "dd天HH時mm分"
         * */
        TextView tv3 = TimerUtils.getTimer(TimerUtils.DEFAULT_STYLE,this,120000000,TimerUtils.TIME_STYLE_FOUR,0)
                .getmDateTv();
        parent.addView(tv3);
        setmLayoutParams(tv3);
        //樣式一倒計時,就是每塊數值和每個間隔分開的樣式,每種樣式下又對應四種時間的格式
        /**
         * 樣式一+時間格式1:JD_STYLE <--> TIME_STYLE_ONE = "HH:mm:ss"
         * */
        TextView tv4= TimerUtils.getTimer(TimerUtils.JD_STYLE,this,120000000,TimerUtils.TIME_STYLE_ONE,R.drawable.timer_shape)
                .setTimerPadding(10,10,10,10)//設定內間距
                .setTimerTextColor(Color.BLACK)//設定字型顏色
                .setTimerTextSize(40)//設定字型大小
                .setTimerGapColor(Color.BLACK)//設定間隔的顏色
                .getmDateTv();//拿到TextView物件
        parent.addView(tv4);
        setmLayoutParams(tv4);
        /**
         * 樣式一+時間格式2:JD_STYLE <--> TIME_STYLE_TWO = "HH時mm分ss秒"
         * */
        TextView tv5= TimerUtils.getTimer(TimerUtils.JD_STYLE,this,120000000,TimerUtils.TIME_STYLE_TWO,R.drawable.timer_shape2)
                .setTimerPadding(10,10,10,10)
                .setTimerTextColor(Color.WHITE)
                .setTimerTextSize(40)
                .setTimerGapColor(Color.BLACK)
                .getmDateTv();
        parent.addView(tv5);
        setmLayoutParams(tv5);
        /**
         * 樣式一+時間格式3:JD_STYLE <-->TIME_STYLE_THREE = "dd天HH時mm分ss秒"
         * */
        TextView tv6= TimerUtils.getTimer(TimerUtils.JD_STYLE,this,120000000,TimerUtils.TIME_STYLE_THREE,R.drawable.timer_shape2)
                .setTimerPadding(10,10,10,10)
                .setTimerTextColor(Color.YELLOW)
                .setTimerTextSize(40)
                .setTimerGapColor(Color.BLACK)
                .getmDateTv();
        parent.addView(tv6);
        setmLayoutParams(tv6);
        /**
         * 樣式一+時間格式4:JD_STYLE <-->TIME_STYLE_FOUR = "dd天HH時mm分"
         * */
        TextView tv7= TimerUtils.getTimer(TimerUtils.JD_STYLE,this,120000000,TimerUtils.TIME_STYLE_FOUR,R.drawable.timer_shape2)
                .setTimerPadding(15,15,15,15)
                .setTimerTextColor(Color.BLUE)
                .setTimerTextSize(40)
                .setTimerGapColor(Color.BLACK)
                .getmDateTv();
        parent.addView(tv7);
        setmLayoutParams(tv7);



        /**
         * 樣式二+時間格式1:VIP_STYLE <-->TIME_STYLE_ONE = "HH:mm:ss"
         * */
        TextView tv8= TimerUtils.getTimer(TimerUtils.VIP_STYLE,this,120000000,TimerUtils.TIME_STYLE_ONE,R.drawable.timer_shape)
                .setTimerPadding(15,15,15,15)
                .setTimerTextColor(Color.BLACK)
                .setTimerTextSize(40)
                .setTimerGapColor(Color.BLACK)
                .getmDateTv();
        parent.addView(tv8);
        setmLayoutParams(tv8);

        /**
         * 樣式二+時間格式2:VIP_STYLE <-->TIME_STYLE_TWO = "HH時mm分ss秒"
         * */
        TextView tv9= TimerUtils.getTimer(TimerUtils.VIP_STYLE,this,120000000,TimerUtils.TIME_STYLE_TWO,R.drawable.timer_shape2)
                .setTimerPadding(15,15,15,15)
                .setTimerTextColor(Color.WHITE)
                .setTimerTextSize(40)
                .setTimerGapColor(Color.BLACK)
                .getmDateTv();
        parent.addView(tv9);
        setmLayoutParams(tv9);
        /**
         * 樣式二+時間格式3:VIP_STYLE <-->TIME_STYLE_THREE = "dd天HH時mm分ss秒"
         * */
        TextView tv10= TimerUtils.getTimer(TimerUtils.VIP_STYLE,this,120000000,TimerUtils.TIME_STYLE_THREE,R.drawable.timer_shape2)
                .setTimerPadding(15,15,15,15)
                .setTimerTextColor(Color.YELLOW)
                .setTimerTextSize(40)
                .setTimerGapColor(Color.BLACK)
                .getmDateTv();
        parent.addView(tv10);
        setmLayoutParams(tv10);
        /**
         * 樣式二+時間格式4:VIP_STYLE <-->TIME_STYLE_FOUR = "dd天HH時mm分"
         * */
        TextView tv11= TimerUtils.getTimer(TimerUtils.VIP_STYLE,this,120000000,TimerUtils.TIME_STYLE_FOUR,R.drawable.timer_shape2)
                .setTimerPadding(15,15,15,15)
                .setTimerTextColor(Color.BLUE)
                .setTimerTextSize(40)
                .setTimerGapColor(Color.BLACK)
                .getmDateTv();
        parent.addView(tv11);
        setmLayoutParams(tv11);
    }

    private void setmLayoutParams(TextView tv) {
        tv.setGravity(Gravity.CENTER_HORIZONTAL);
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) tv.getLayoutParams();
        params.setMargins(20,20,20,20);
        tv.setLayoutParams(params);
    }
}

兩個drawable檔案:

帶邊框樣式

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle"
    >
    <corners android:radius="5px"/>
    <stroke android:color="#88000000" android:width="1dp"/>
</shape>
帶背景和邊框樣式
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle"
    >
    <corners android:radius="10px"/>
    <solid android:color="#000000"/>
</shape>

現在就看看我們執行的成果吧。


看看執行結果還不錯吧,其實它的樣式還可以定義很多種主要看自己的創意和想法了,這個倒計時封裝如果還有什麼不足之處,請多多提出建議。但是現在使用還是蠻方便和簡單的,一行程式碼就能就能解決。這個倒計時用到的地方還是蠻多的,大家有需要的話可以直接引入到自己的專案中。

PS:有人反應說Demo放入工程跑起來有問題,但是我自己是沒問題的,然後我索性將這個打成了一個aar包,只需要將這個aar包放入工程中,即可在Activity中如部落格上所寫一樣通過一行程式碼即可實現倒計時。注意:aar包只適用於AndroidStudio

引入說明:

新建一個Module,將aar包放入libs目錄,然後在build.gradle中新增:

repositories {
    flatDir {
        dirs 'libs'
}
}
repositories放入android{}內部
compile(name:'mikyoutimerlib',ext:'aar')
compile放入dependencies {}內部,name-->aar包名
aar包下載