1,最近打開keep的app的時候,發現它的歡迎頁面的倒計時效果還不錯,所以打算自己來寫寫,然后就有了這篇文章。
2,還是老規矩,先看一下我們今天實現的效果

相較於我們常見的倒計時,這次實現的效果是多了外面圓環的不斷減少,這也是我們這次自定義view的有意思的一點。
知道了效果我們先來效果分析一波,首先是一個倒計時效果,計時的時候上面的圓弧不斷的減少,里面的文字也不斷的變化,在視覺上的改變就大致為這兩部分,但是實際上我們的ui是由三部分來構成的:里面的實心圓、外面的圓弧、里面的文字。知道了我們ui的組成,我們就來開擼開擼。
在開擼之前我們還是回顧一下我們簡單的自定義view的基本流程
/** * 自定義View的幾個步驟 * 1,自定義View屬性 * 2,在View中獲得我們的自定義的屬性 * 3,重寫onMeasure * 4,重寫onDraw * 5,重寫onLayout(這個決定view放置在哪兒) */
①、確定自定義屬性
我們根據上面的基本步驟,我們知道首先我們根據效果圖先來確定我們這次的自定義屬性,這里我簡單的分析了一下,主要添加了八個自定義屬性,分別是里面實心圓的半徑和顏色、圓弧的顏色和半徑、里面文字的大小和顏色、總倒計時時間的長度、圓弧減少的方向(分為順時針和逆時針),所以首先在res/values目錄下創建attrs.xml文件,添加以下屬性:(這里如果有對自定義屬性不太了解的同學可以去了解我以前寫過的這篇文章,可以更加深刻的理解)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleTimerView">
<attr name="solid_circle_radius" format="dimension"/>
<attr name="solid_circle_color" format="color"/>
<attr name="empty_circle_color" format="color"/>
<attr name="empty_circle_radius" format="dimension"/>
<attr name="circle_text_size" format="dimension"/>
<attr name="circle_text_color" format="color"/>
<attr name="circle_draw_orientation" format="enum">
<!--順時針-->
<enum name="clockwise" value="1"/>
<!--逆時針-->
<enum name="anticlockwise" value="2"/>
</attr>
<attr name="time_length" format="integer"/>
</declare-styleable>
</resources>
②、獲取自定義屬性、初始化一些屬性
首先創建CircleTimerView類,繼承自View類
public class CircleTimerView extends View {
private Context context ;
//里面實心圓顏色
private int mSolidCircleColor ;
//里面圓的半徑
private int mSolidCircleRadius;
//外面圓弧的顏色
private int mEmptyCircleColor ;
//外面圓弧的半徑(可以使用畫筆的寬度來實現)
private int mEmptyCircleRadius ;
//文字大小
private int mTextSize ;
//文字顏色
private int mTextColor ;
//文字
private String mText ;
//繪制的方向
private int mDrawOrientation;
//圓弧繪制的速度
private int mSpeed;
//圓的畫筆
private Paint mPaintCircle ;
//圓弧的畫筆
private Paint mPaintArc ;
//繪制文字的畫筆
private Paint mPaintText;
//時長
private int mTimeLength ;
//默認值
private int defaultSolidCircleColor ;
private int defaultEmptyCircleColor ;
private int defaultSolidCircleRadius ;
private int defaultEmptyCircleRadius ;
private int defaultTextColor ;
private int defaultTextSize ;
private int defaultTimeLength ;
private int defaultDrawOritation ;
//當前扇形的角度
private int startProgress ;
private int endProgress ;
private float currProgress ;
//動畫集合
private AnimatorSet set ;
//回調
private OnCountDownFinish onCountDownFinish ;
public CircleTimerView(Context context) {
this(context,null);
}
public CircleTimerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public CircleTimerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context ;
//初始化默認值
defaultSolidCircleColor = getResources().getColor(R.color.colorPrimary);
defaultEmptyCircleColor = getResources().getColor(R.color.colorAccent);
defaultTextColor = getResources().getColor(R.color.colorYellow);
defaultSolidCircleRadius = (int) getResources().getDimension(R.dimen.dimen_20);
defaultEmptyCircleRadius = (int) getResources().getDimension(R.dimen.dimen_25);
defaultTextSize = (int) getResources().getDimension(R.dimen.dimen_16);
defaultTimeLength = 3 ;
defaultDrawOritation = 1 ;
//獲取自定義屬性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleTimerView);
mSolidCircleColor = a.getColor(R.styleable.CircleTimerView_solid_circle_color,defaultSolidCircleColor);
mSolidCircleRadius = a.getDimensionPixelOffset(R.styleable.CircleTimerView_solid_circle_radius ,defaultSolidCircleRadius);
mEmptyCircleColor = a.getColor(R.styleable.CircleTimerView_empty_circle_color,defaultEmptyCircleColor);
mEmptyCircleRadius = a.getDimensionPixelOffset(R.styleable.CircleTimerView_empty_circle_radius ,defaultEmptyCircleRadius);
mTextColor = a.getColor(R.styleable.CircleTimerView_circle_text_color,defaultTextColor);
mTextSize = a.getDimensionPixelOffset(R.styleable.CircleTimerView_circle_text_size ,defaultTextSize);
mDrawOrientation = a.getInt(R.styleable.CircleTimerView_circle_draw_orientation,defaultDrawOritation);
mTimeLength = a.getInt(R.styleable.CircleTimerView_time_length ,defaultTimeLength);
a.recycle();
init();
}
private void init() {
//初始化畫筆
mPaintCircle = new Paint();
mPaintCircle.setStyle(Paint.Style.FILL);
mPaintCircle.setAntiAlias(true);
mPaintCircle.setColor(mSolidCircleColor);
mPaintArc = new Paint();
mPaintArc.setStyle(Paint.Style.STROKE);
mPaintArc.setAntiAlias(true);
mPaintArc.setColor(mEmptyCircleColor);
mPaintArc.setStrokeWidth(mEmptyCircleRadius - mSolidCircleRadius);
mPaintText = new Paint();
mPaintText.setStyle(Paint.Style.STROKE);
mPaintText.setAntiAlias(true);
mPaintText.setTextSize(mTextSize);
mPaintText.setColor(mTextColor);
mText= mTimeLength +"" ;
if(defaultDrawOritation == 1){
startProgress = 360 ;
endProgress = 0 ;
}else {
startProgress = 0 ;
endProgress = 360 ;
}
currProgress = startProgress ;
}
這里我在構造函數里面先初始化一些默認的值,然后獲取自定義屬性,然后再初始化三個畫筆,分別代表:實心圓、圓弧、Text的畫筆(這個很好理解),然后根據順時針和逆時針來初始化開始角度和結束角度,很簡單就不在過多的廢話了。
③、重寫onMeasure方法
這里由於我們的效果很簡單,基本上就是一個正方形,所以這里我是以外面圓弧的半徑當這個view 的寬高的,就沒去判斷match_parent、wrap_content之類的情況,代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//設置寬高
setMeasuredDimension(mEmptyCircleRadius*2,mEmptyCircleRadius*2);
}
④,重寫onDraw方法
這也是我們自定義view關鍵,首先我們繪制圓弧和文字很簡單,繪制圓弧的話可能有些同學沒有接觸過,這里我以前寫過一篇,大家可以去看看,我們這里要用的知識點 都是一樣的,所以就不再廢話
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪制背景圓
canvas.drawCircle(mEmptyCircleRadius,mEmptyCircleRadius,mSolidCircleRadius,mPaintCircle);
//繪制圓弧
RectF oval = new RectF((mEmptyCircleRadius - mSolidCircleRadius)/2, (mEmptyCircleRadius - mSolidCircleRadius)/2
, mEmptyCircleRadius + (mEmptyCircleRadius - mSolidCircleRadius)/2+mSolidCircleRadius, mEmptyCircleRadius + (mEmptyCircleRadius - mSolidCircleRadius)/2+mSolidCircleRadius); // 用於定義的圓弧的形狀和大小的界限
canvas.drawArc(oval, -90, currProgress, false, mPaintArc); // 根據進度畫圓弧
//繪制文字
Rect mBound = new Rect();
mPaintText.getTextBounds(mText, 0, mText.length(), mBound);
canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaintText);
}
在這個時候,我們就可以來看一下我們自定義view的效果了,將我們currProgress先寫死成270,來看看我們的效果,這里注意一項在使用我們的自定義屬性的時候,記得在布局文件中添加我們自定義空間。運行效果如下:

可以看到這里我們的效果基本上試出來了,關鍵是怎么讓它動起來,這里我們的第一反應是handle或者timer來實現一個倒計時,一開始阿呆哥哥也是使用timer來實現的,不過發現由於ui的改變中是有兩個不同速率的view在改變:圓弧的不斷減小、textView字體的逐漸變小,所以這里使用一個timer無法實現,得用兩個,如果用兩個就不怎么軟件工程了,所以這里打算使用動畫來實現,具體代碼如下:
/**
* 通過外部開關控制
*/
public void start(){
ValueAnimator animator1 = ValueAnimator.ofFloat(startProgress,endProgress);
animator1.setInterpolator(new LinearInterpolator());
animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
currProgress = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
ValueAnimator animator2 = ValueAnimator.ofInt(mTimeLength,0);
animator2.setInterpolator(new LinearInterpolator());
animator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mTimeLength = (int) valueAnimator.getAnimatedValue();
if (mTimeLength == 0)
return;
mText =mTimeLength+ "";
}
});
set = new AnimatorSet();
set.playTogether(animator1,animator2);
set.setDuration(mTimeLength * 1000);
set.start();
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
if (onCountDownFinish != null){
onCountDownFinish.onFinish();
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
}
很簡單,就是兩個ValueAnimator,監聽值的改變,然后再最后完成的動畫的時候使用接口回調,通知宿主完成ToDo操作,所以到這里我們基本上完全實現了我們的view 的自定義,CircleTimerView的完整代碼如下:
package com.ysten.circletimerdown.view;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AnimationSet;
import android.view.animation.LinearInterpolator;
import com.ysten.circletimerdown.R;
import java.util.Timer;
import java.util.TimerTask;
/**
* author : wangjitao
* e-mail : 543441727@qq.com
* time : 2017/08/14
* desc :
* version: 1.0
*/
public class CircleTimerView extends View {
private Context context ;
//里面實心圓顏色
private int mSolidCircleColor ;
//里面圓的半徑
private int mSolidCircleRadius;
//外面圓弧的顏色
private int mEmptyCircleColor ;
//外面圓弧的半徑(可以使用畫筆的寬度來實現)
private int mEmptyCircleRadius ;
//文字大小
private int mTextSize ;
//文字顏色
private int mTextColor ;
//文字
private String mText ;
//繪制的方向
private int mDrawOrientation;
//圓弧繪制的速度
private int mSpeed;
//圓的畫筆
private Paint mPaintCircle ;
//圓弧的畫筆
private Paint mPaintArc ;
//繪制文字的畫筆
private Paint mPaintText;
//時長
private int mTimeLength ;
//默認值
private int defaultSolidCircleColor ;
private int defaultEmptyCircleColor ;
private int defaultSolidCircleRadius ;
private int defaultEmptyCircleRadius ;
private int defaultTextColor ;
private int defaultTextSize ;
private int defaultTimeLength ;
private int defaultDrawOritation ;
//當前扇形的角度
private int startProgress ;
private int endProgress ;
private float currProgress ;
//動畫集合
private AnimatorSet set ;
//回調
private OnCountDownFinish onCountDownFinish ;
public CircleTimerView(Context context) {
this(context,null);
}
public CircleTimerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public CircleTimerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context ;
//初始化默認值
defaultSolidCircleColor = getResources().getColor(R.color.colorPrimary);
defaultEmptyCircleColor = getResources().getColor(R.color.colorAccent);
defaultTextColor = getResources().getColor(R.color.colorYellow);
defaultSolidCircleRadius = (int) getResources().getDimension(R.dimen.dimen_20);
defaultEmptyCircleRadius = (int) getResources().getDimension(R.dimen.dimen_25);
defaultTextSize = (int) getResources().getDimension(R.dimen.dimen_16);
defaultTimeLength = 3 ;
defaultDrawOritation = 1 ;
//獲取自定義屬性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleTimerView);
mSolidCircleColor = a.getColor(R.styleable.CircleTimerView_solid_circle_color,defaultSolidCircleColor);
mSolidCircleRadius = a.getDimensionPixelOffset(R.styleable.CircleTimerView_solid_circle_radius ,defaultSolidCircleRadius);
mEmptyCircleColor = a.getColor(R.styleable.CircleTimerView_empty_circle_color,defaultEmptyCircleColor);
mEmptyCircleRadius = a.getDimensionPixelOffset(R.styleable.CircleTimerView_empty_circle_radius ,defaultEmptyCircleRadius);
mTextColor = a.getColor(R.styleable.CircleTimerView_circle_text_color,defaultTextColor);
mTextSize = a.getDimensionPixelOffset(R.styleable.CircleTimerView_circle_text_size ,defaultTextSize);
mDrawOrientation = a.getInt(R.styleable.CircleTimerView_circle_draw_orientation,defaultDrawOritation);
mTimeLength = a.getInt(R.styleable.CircleTimerView_time_length ,defaultTimeLength);
a.recycle();
init();
}
private void init() {
//初始化畫筆
mPaintCircle = new Paint();
mPaintCircle.setStyle(Paint.Style.FILL);
mPaintCircle.setAntiAlias(true);
mPaintCircle.setColor(mSolidCircleColor);
mPaintArc = new Paint();
mPaintArc.setStyle(Paint.Style.STROKE);
mPaintArc.setAntiAlias(true);
mPaintArc.setColor(mEmptyCircleColor);
mPaintArc.setStrokeWidth(mEmptyCircleRadius - mSolidCircleRadius);
mPaintText = new Paint();
mPaintText.setStyle(Paint.Style.STROKE);
mPaintText.setAntiAlias(true);
mPaintText.setTextSize(mTextSize);
mPaintText.setColor(mTextColor);
mText= mTimeLength +"" ;
if(defaultDrawOritation == 1){
startProgress = 360 ;
endProgress = 0 ;
}else {
startProgress = 0 ;
endProgress = 360 ;
}
currProgress = startProgress ;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//設置寬高
setMeasuredDimension(mEmptyCircleRadius*2,mEmptyCircleRadius*2);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪制背景圓
canvas.drawCircle(mEmptyCircleRadius,mEmptyCircleRadius,mSolidCircleRadius,mPaintCircle);
//繪制圓弧
RectF oval = new RectF((mEmptyCircleRadius - mSolidCircleRadius)/2, (mEmptyCircleRadius - mSolidCircleRadius)/2
, mEmptyCircleRadius + (mEmptyCircleRadius - mSolidCircleRadius)/2+mSolidCircleRadius, mEmptyCircleRadius + (mEmptyCircleRadius - mSolidCircleRadius)/2+mSolidCircleRadius); // 用於定義的圓弧的形狀和大小的界限
canvas.drawArc(oval, -90, currProgress, false, mPaintArc); // 根據進度畫圓弧
//繪制文字
Rect mBound = new Rect();
mPaintText.getTextBounds(mText, 0, mText.length(), mBound);
canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaintText);
}
public OnCountDownFinish getOnCountDownFinish() {
return onCountDownFinish;
}
public void setOnCountDownFinish(OnCountDownFinish onCountDownFinish) {
this.onCountDownFinish = onCountDownFinish;
}
/**
* 通過外部開關控制
*/
public void start(){
ValueAnimator animator1 = ValueAnimator.ofFloat(startProgress,endProgress);
animator1.setInterpolator(new LinearInterpolator());
animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
currProgress = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
ValueAnimator animator2 = ValueAnimator.ofInt(mTimeLength,0);
animator2.setInterpolator(new LinearInterpolator());
animator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mTimeLength = (int) valueAnimator.getAnimatedValue();
if (mTimeLength == 0)
return;
mText =mTimeLength+ "";
}
});
set = new AnimatorSet();
set.playTogether(animator1,animator2);
set.setDuration(mTimeLength * 1000);
set.start();
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
if (onCountDownFinish != null){
onCountDownFinish.onFinish();
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
}
public void cancelAnim(){
if(set != null)
set.pause();
}
public interface OnCountDownFinish{
void onFinish();
}
}
最后實現的效果如下:

Github代碼地址,有需要源碼的同學可以去下載一下。
