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步骤 ...
随机推荐
- [leetcode-560-Subarray Sum Equals K]
Given an array of integers and an integer k, you need to find the total number of continuous subarra ...
- 【LeetCode】235. Lowest Common Ancestor of a Binary Search Tree
题目: Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in th ...
- Java学习笔记之字符串常用方法
一.String关键字一些常用方法 1.构造方法: public String(); 空构造 public String(byte[] bytes);将字节数组转成字符串 public String ...
- Linux 安装依赖库
###安装依赖库###yum -y install rsync net-snmp syslog net-snmp-devel wget patch screen gcc gcc-c++ autocon ...
- BufferedWriterTest
public class BufferedWriterTest { public static void main(String[] args) { try { //创建一个FileWriter 对象 ...
- poj 2739 Sum of Consecutive Prime Numbers 小结
Description Some positive integers can be represented by a sum of one or more consecutive prime num ...
- P1280 尼克的任务
题目描述 尼克每天上班之前都连接上英特网,接收他的上司发来的邮件,这些邮件包含了尼克主管的部门当天要完成的全部任务,每个任务由一个开始时刻与一个持续时间构成. 尼克的一个工作日为N分钟,从第一分钟开始 ...
- Java 异常处理笔记
Java程序运行过程中所发生的异常事件可分为两类: §错误(Error):JVM系统内部错误.资源耗尽等严重情况 §违例(Exception): 其它因编程错误或偶然的外在因素导致的一般性问题,例如: ...
- 腾讯云centos7服务器环境搭建,tomcat+jdk+mysql+nginx
软件:jdk 1.8.0_45 tomcat 8.5.8 mysql 5.6.36 nginx 1.10.x或以上 其中tomcat在centos6.8中没问题,centos7中会出现卡在启动那里 I ...
- 50行代码实现的高性能动画定时器 raf-interval
写在前面 raf-interval 是基于 window.requestAnimationFrame() 封装的定时器. Github: https://github.com/dntzhang/raf ...