Android -- 自定义view实现keep欢迎页倒计时效果
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代码地址,有需要源码的同学可以去下载一下。