1,最近打開keep的app的時候,發現它的歡迎頁面的倒計時效果還不錯,因此打算本身來寫寫,而後就有了這篇文章。html
2,仍是老規矩,先看一下咱們今天實現的效果java
相較於咱們常見的倒計時,此次實現的效果是多了外面圓環的不斷減小,這也是咱們此次自定義view的有意思的一點。android
知道了效果咱們先來效果分析一波,首先是一個倒計時效果,計時的時候上面的圓弧不斷的減小,裏面的文字也不斷的變化,在視覺上的改變就大體爲這兩部分,可是實際上咱們的ui是由三部分來構成的:裏面的實心圓、外面的圓弧、裏面的文字。知道了咱們ui的組成,咱們就來開擼開擼。git
在開擼以前咱們仍是回顧一下咱們簡單的自定義view的基本流程github
/** * 自定義View的幾個步驟 * 1,自定義View屬性 * 2,在View中得到咱們的自定義的屬性 * 3,重寫onMeasure * 4,重寫onDraw * 5,重寫onLayout(這個決定view放置在哪兒) */
①、肯定自定義屬性canvas
咱們根據上面的基本步驟,咱們知道首先咱們根據效果圖先來肯定咱們此次的自定義屬性,這裏我簡單的分析了一下,主要添加了八個自定義屬性,分別是裏面實心圓的半徑和顏色、圓弧的顏色和半徑、裏面文字的大小和顏色、總倒計時時間的長度、圓弧減小的方向(分爲順時針和逆時針),因此首先在res/values目錄下建立attrs.xml文件,添加如下屬性:(這裏若是有對自定義屬性不太瞭解的同窗能夠去了解我之前寫過的這篇文章,能夠更加深入的理解)app
<?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>
②、獲取自定義屬性、初始化一些屬性ide
首先建立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代碼地址,有須要源碼的同窗能夠去下載一下。