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代码地址,有需要源码的同学可以去下载一下。

Android -- 自定义view实现keep欢迎页倒计时效果的更多相关文章

  1. android自定义view仿照MIUI中音量控制效果

    先看效果图: 这就是miui中的音量效果图,实现思路是自定义视图,绘制圆环,然后设置进度显示. 核心代码在onDraw中实现如下: @Override protected void onDraw(Ca ...

  2. Android自定义View——简单实现边缘凹凸电子票效果

        View继承LinearLayout,在View的上下边缘画出白色的圆形即可,这里只要计算出圆的个数和圆的循环规律即可,下面请看分析 我们取卡片的前2个凹凸来看,将其分为四部分,并且两部分为循 ...

  3. Android自定义View——贝塞尔曲线实现水波纹效果

    我们使用到的是Path类的quadTo(x1, y1, x2, y2)方法,属于二阶贝塞尔曲线,使用一张图来展示二阶贝塞尔曲线,这里的(x1,y1)是控制点,(x2,y2)是终止点,起始点默认是Pat ...

  4. Android 自定义View 之利用ViewPager 实现画廊效果(滑动放大缩小)

    http://www.2cto.com/kf/201608/542107.html

  5. Android 自定义View合集

    自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...

  6. android自定义View之NotePad出鞘记

    现在我们的手机上基本都会有一个记事本,用起来倒也还算方便,记事本这种东东,如果我想要自己实现,该怎么做呢?今天我们就通过自定义View的方式来自定义一个记事本.OK,废话不多说,先来看看效果图. 整个 ...

  7. Android自定义View和控件之一-定制属于自己的UI

    照例,拿来主义.我的学习是基于下面的三篇blog.前两是基本的流程,第三篇里有比较细致的绘制相关的属性.第4篇介绍了如何减少布局层次来提高效率. 1. 教你搞定Android自定义View 2. 教你 ...

  8. Android自定义View 画弧形,文字,并增加动画效果

    一个简单的Android自定义View的demo,画弧形,文字,开启一个多线程更新ui界面,在子线程更新ui是不允许的,但是View提供了方法,让我们来了解下吧. 1.封装一个抽象的View类   B ...

  9. (转)[原] Android 自定义View 密码框 例子

    遵从准则 暴露您view中所有影响可见外观的属性或者行为. 通过XML添加和设置样式 通过元素的属性来控制其外观和行为,支持和重要事件交流的事件监听器 详细步骤见:Android 自定义View步骤 ...

随机推荐

  1. SVN仓库迁移到Git的完美解决办法

    参考文章Converting a Subversion repository to Git 1 使用git svn clone 拷贝svn仓库 cd ~/test_repo git svn clone ...

  2. Kanzi 倒影效果制作

    在kanzi中,倒影效果会经常用到,比如多媒体中. 先来看一下最终的实现效果: 在这个效果中,我们的需求是,倒影图与原图一致,透明度和可见范围可以调节. 下面说一下实现的步骤: 1.创建工程后,Roo ...

  3. 【知识整理】这可能是最好的RxJava 2.x 教程(完结版)

    为什么要学 RxJava? 提升开发效率,降低维护成本一直是开发团队永恒不变的宗旨.近两年来国内的技术圈子中越来越多的开始提及 RxJava ,越来越多的应用和面试中都会有 RxJava ,而就目前的 ...

  4. 使用ReflectionToStringBuilder实现toString方法

    使用ReflectionToStringBuilder实现toString方法 org.apache.commons.lang.builder.ReflectionToStringBuilder是co ...

  5. CentOS 下搭建FTP服务器

    vsftpd是Linux下比较著名的FTP服务器,搭建FTP服务器当然首选这个.本文介绍了在CentOS 6 4下安装vsftpd.配置虚拟用户登录FTP的过程.正 vsftpd是Linux下比较著名 ...

  6. Java IO学习笔记(三)转换流、数据流、字节数组流

    转换流 1.转换流:将字节流转换成字符流,转换之后就可以一个字符一个字符的往程序写内容了,并且可以调用字符节点流的write(String s)方法,还可以在外面套用BufferedReader()和 ...

  7. FZU 1015 土地划分

        Description 在Dukeswood这块土地上生活着一个富有的农庄主和他的几个孩子.在他临终时,他想把他的土地分给他的孩子.他有许多农场,每个农场都是一块矩形土地.他在农场地图上划上一 ...

  8. Struts 之 通配符 路径匹配 常量用法 配置默认值

    Struts 框架学习 Action的开发的几种方式 方式1 : 继承ActionSupport     如果使用Struts校验功能,必须继承此类 方式2 : 实现Action接口 方式3 :不继承 ...

  9. Css绘制形状

    前言:终于我的大一生活结束了,迎来了愉快的暑假,大家都开始了各自的忙碌.一直忙着一些项目的事情,终于决定今天要更新一篇博客了,对上一阶段的学习做简单的总结. 这次我主要总结一下用Css绘制各种形状的技 ...

  10. centos7 安装elasticsearch

    [root@localhost local]# tar xzvf elasticsearch-2.3.5.tar.gz [root@localhost elasticsearch-2.3.5]# bi ...