首页 > 代码库 > 浅谈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>

现在就看看我们运行的成果吧。

技术分享

看看运行结果还不错吧,其实它的样式还可以定义很多种主要看自己的创意和想法了,这个倒计时封装如果还有什么不足之处,请多多提出建议。但是现在使用还是蛮方便和简单的,一行代码就能就能解决。这个倒计时用到的地方还是蛮多的,大家有需要的话可以直接引入到自己的项目中。

Demo下载










          





浅谈android中仅仅使用一个TextView实现高仿京东,淘宝各种倒计时