深度剖析之 CountdownView

剖析项目名称: CountdownView
剖析原项目地址https://github.com/iwgang/CountdownView
剖析理由只知其然而不知其所以然,如此不好。想要快速的进阶,不走寻常路,剖析开源项目,深入理解扩展知识,仅仅这样还不够,还需要如此:左手爱哥的设计模式,右手重构改善既有设计,如此漫长打坐,回过头再看来时的路,书已成山,相信翔哥说的,量变引起质变。


在不久前做的一个商城项目,有抢购活动的需求,活动没开始要倒计时,开始后到结束也要倒计时,虽然当时我捣鼓了一个自定义控件,效果也出来了,但是赶脚该控件写的太复杂了,逻辑调理不清晰,郁闷不已,前两天又发现该开源项目,下定决心学习一下,顺便重新梳理知识。下面上效果图:

CountdownView是一个Android 倒计时控件,使用Canvas绘制,支持多种样式,Android Studio导入:

compile 'com.github.iwgang:countdownview:1.2'

代码调用实例:

CountdownView mCvCountdownView = (CountdownView)findViewById(R.id.cv_countdownViewTest1);
mCvCountdownView.start(995550000);
/**或者自己编写倒计时逻辑,然后调用updateShow来更新UI*/
for (int time=0; time<1000; time++) {
    mCvCountdownView.updateShow(time);
}

xxx.xml引用实例:

<cn.iwgang.countdownview.CountdownView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:isHideTimeBackground="true"
    app:isShowDay="true"
    app:isShowHour="true"
    app:isShowMinute="true"
    app:isShowSecond="true"
    app:isShowMillisecond="true"
    app:timeTextColor="#000000"
    app:timeTextSize="22sp"
    app:isTimeTextBold="true"
    app:suffixGravity="bottom"
    app:suffixTextColor="#000000"
    app:suffixTextSize="12sp"
    app:suffixHour="时"
    app:suffixMinute="分"
    app:suffixSecond="秒"
    app:suffixMillisecond="毫秒" />

下面是相关自定义的属性表(属性名称、类型、默认值)

isHideTimeBackground    boolean true 隐藏倒计时背景
timeBgColor color   #444444 倒计时的背景色
timeBgSize  dimension   timeSize + 2dp * 4 倒计时背景大小
timeBgRadius    dimension   0 倒计时背景的圆角
isShowTimeBgDivisionLine    boolean true 倒计时的横向的分割线
timeBgDivisionLineColor color   #30FFFFFF 倒计时的横向的分割线颜色
timeBgDivisionLineSize  dimension   0.5dp 倒计时的横向的分割线高度
timeTextSize    dimension   12sp 倒计时的文字大小
timeTextColor   color   #000000 倒计时的文字颜色
isTimeTextBold  boolean false 倒计时文字所在的边框
isShowDay   boolean 自动显示 (天 > 1 显示, = 0 隐藏)
isShowHour  boolean 自动显示 (小时 > 1 显示, = 0 隐藏)
isShowMinute    boolean true 是否显示分钟
isShowSecond    boolean true 是否显示秒
isShowMillisecond   boolean false 是否显示毫秒
suffixTextSize  dimension   12sp 添加的分号:的大小
suffixTextColor color   #000000 添加的分号:的颜色
isSuffixTextBold    boolean false 添加的分号:的边框
suffixGravity   'top' or 'center' or 'bottom'   'center' 添加的分号:对齐方式
suffix  string  ':' 添加的分号:默认值
suffixDay   string  null 天默认值
suffixHour  string  null 时默认值
suffixMinute    string  null 分默认值
suffixSecond    string  null 秒默认值
suffixMillisecond   string  null 毫秒默认值
suffixLRMargin  dimension   left 3dp right 3dp 间距默认左右各3dp
suffixDayLeftMargin dimension   0
suffixDayRightMargin    dimension   0
suffixHourLeftMargin    dimension   0
suffixHourRightMargin   dimension   0
suffixMinuteLeftMargin  dimension   0
suffixMinuteRightMargin dimension   0
suffixSecondLeftMargin  dimension   0
suffixSecondRightMargin dimension   0
suffixMillisecondLeftMargin dimension   0

由上面一堆自定义属性来看,哎妈呀,好多属性,构造函数的解析好多,真心感觉原作者他好累!!
当我么ListView 或者RecycleView等控件使用倒计时,会出现这个情况一个界面有多个倒计时,那么他的回调函数在Activity、Fragment里面只有一个,这是后我们可以选择添加Tag标签,回调函数onEnd里判断Tag值匹配执行响应函数,个人觉得在Adapter里面使用有个更好的方法,onEnd(CountdownView ,position)把position回调回来就简单多了,这里不细说,先看给控件添加Tag回调实例:

    // 第1步,设置tag
    mCvCountdownView.setTag(R.id.name, uid);
    // 第2步,从回调中的CountdownView取回tag
    @Override
    public void onEnd(CountdownView cv) {
        Object nameTag = cv.getTag(R.id.uid);
        if (null != nameTag) {
            Log.i(TAG, "name = " + nameTag.toString());
        }
    }

动态显示/隐藏某些时间 (如:开始显示时、分、秒,后面到指定时间改成分、秒、毫秒)

customTimeShow(boolean isShowDay, 
               boolean isShowHour,
               boolean  isShowMinute,
               boolean isShowSecond,
               boolean isShowMillisecond)

指定间隔时间回调和倒计时结束回调:

setOnCountdownIntervalListener(long interval, OnCountdownIntervalListener onCountdownIntervalListener);
setOnCountdownEndListener(OnCountdownEndListener onCountdownEndListener);

倒计时的抽象类CustomCountDownTimer ,调用其start、stop(同步方法)方法控制倒计时,Handler控制进度,重点在这里:

 while (delay < 0) delay += mCountdownInterval;

 sendMessageDelayed(obtainMessage(MSG), delay);

倒计时没结束继续发消息while循环直到结束,下面再来看正主CountdownView:

public class CountdownView extends View {
    public CountdownView(Context context) {
        this(context, null);
    }

    public CountdownView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CountdownView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CountdownView);
        mTimeBgColor = ta.getColor(R.styleable.CountdownView_timeBgColor, 0xFF444444);
        mTimeBgRadius = ta.getDimension(R.styleable.CountdownView_timeBgRadius, 0);

       //..........................属性解析略...........................
        }
}

构造方法调用initPaint初始化画笔操作,两个个人平时不常用属性:setFakeBoldText(true);//true设定,false清除,这里是设置中文仿“粗体”-.setTextAlign(Paint.Align.CENTER);设置文字对齐方式,系统提供了几种,当这些不满足我们需求比如盖章那种类型的就需要Path配合绘制完成,这里不多说

   private void initPaint() {
        // time text
        mTimeTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTimeTextPaint.setColor(mTimeTextColor);
        mTimeTextPaint.setTextAlign(Paint.Align.CENTER);
        mTimeTextPaint.setTextSize(mTimeTextSize);
        if (isTimeTextBold) {
            mTimeTextPaint.setFakeBoldText(true);
        }
        //..........................更多Paint初始化略...........................
    }

initSuffix(true);根据:天时分秒毫秒等是否显示初始化值,initSuffixMargin()初始化边距值,用到了dp转px,sp转px,真心佩服写着控件的主人,耐心真好!!初始化还在继续,测量文字以便于onDraw绘制(有人说这种测量方法有误差,配合mPaint.measureText()就完美了)

  Rect rect = new Rect();
        mTimeTextPaint.getTextBounds("00", 0, 2, rect);
        mTimeTextWidth = rect.width();
        mTimeTextHeight = rect.height();
        mTimeTextBottom = rect.bottom;

构造函数说完该轮到onMeasure测量函数了

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //根据现实的元素计算总宽度
        mContentAllWidth = getAllContentWidth();
        //根据现实的元素计算总高度
        mContentAllHeight = isHideTimeBackground ? (int) mTimeTextHeight : (int) mTimeBgSize;
        //更具传入类型和MeasureSpec获取响应的mode size,计算宽高
        mViewWidth = measureSize(1, mContentAllWidth, widthMeasureSpec);
        mViewHeight = measureSize(2, mContentAllHeight, heightMeasureSpec);
        //测量到控件的宽高后重新设置(setMeasuredDimension方法觉得view视图大小) 
        setMeasuredDimension(mViewWidth, mViewHeight);
        //又开始一些列初始化了,计算这些值好累O(∩_∩)O~
        initTimeTextBaselineAndTimeBgTopPadding();
        initLeftPaddingSize();
        initTimeBgRect();
    }

measure测量个人感觉下面这种方式比较喜欢,以前我都全部一股脑的写在了onMeasure里面

private int measureSize(int specType, int contentSize, int measureSpec) {
        int result;
        //获取测量的模式和Size
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            result = Math.max(contentSize, specSize);
        } else {
            result = contentSize;

            if (specType == 1) {
                // 根据传人方式计算宽
                result += (getPaddingLeft() + getPaddingRight());
            } else {
                // 根据传人方式计算高
                result += (getPaddingTop() + getPaddingBottom());
            }
        }

        return result;
    }

太多的初始化操作了,太多的属性了,我终于发现我和大神的区别了,我的耐心和他们没法比,哎..再来看你onDraw绘制:

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        float mHourLeft;
        float mMinuteLeft;
        float mSecondLeft;

        if (isHideTimeBackground) {
            // no background
           //*****drawText 时分秒等略***
                canvas.drawText(formatNum(mDay), mLeftPaddingSize + mDayTimeTextWidth / 2, mTimeTextBaseline, mTimeTextPaint);
            //****根据是否显示时分秒等控制是否绘制*******
            if (isShowSecond) {
                // draw second text
                canvas.drawText(formatNum(mSecond), mSecondLeft + mTimeTextWidth / 2, mTimeTextBaseline, mTimeTextPaint);


    } else {

            // 要绘制背景

            if (isShowDay) {
                // 绘制带圆角的
                canvas.drawRoundRect(mDayBgRectF, mTimeBgRadius, mTimeBgRadius, mTimeTextBgPaint);
                绘制横向的分割线
                if (isShowTimeBgDivisionLine) {
                    // draw day background division line
                    canvas.drawLine(mLeftPaddingSize, mTimeBgDivisionLineYPos, mLeftPaddingSize + mDayTimeBgWidth, mTimeBgDivisionLineYPos, mTimeTextBgDivisionLinePaint);
                }
               //********************代码绘制太多都略过吧*********************
    }

在我们调用countdownView.start方法本质是调用了辅助类的start方法:

/**
     * start countdown
     * @param millisecond millisecond
     */
    public void start(long millisecond) {
        if (millisecond <= 0) {
            return ;
        }

        if (null != mCustomCountDownTimer) {
            mCustomCountDownTimer.stop();
            mCustomCountDownTimer = null;
        }

        long countDownInterval;
        if (isShowMillisecond) {
            countDownInterval = 10;
            updateShow(millisecond);
        } else {
            countDownInterval = 1000;
        }

        mCustomCountDownTimer = new CustomCountDownTimer(millisecond, countDownInterval) {
            @Override
            public void onTick(long millisUntilFinished) {
                updateShow(millisUntilFinished);
            }

            @Override
            public void onFinish() {
                // countdown end
                allShowZero();
                // 倒计时结束了回调该函数
                if (null != mOnCountdownEndListener) {
                    mOnCountdownEndListener.onEnd(CountdownView.this);
                }
            }
        };
        mCustomCountDownTimer.start();
    }

stop同理调用了辅助类的stop方法这里说了,这个开源项目看完,学到了许多,收获最大的是辅助类的运用,把倒计时时间分离到抽象类,自身需要处理的用接口回调,内部实例化辅助类,这样的代码结构清晰简洁,真实我需要的,我当时写这个类型控件全放在一个类里处理,乱糟糟的感觉。


参考资料
http://blog.csdn.net/mapdigit/article/details/7784035

*逗比的一天终于结束了,嘴强王者回家开鲁啦~O(∩_∩)O~*

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值