自定义控件(倒计时篇)
Posted WYH_Healer
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义控件(倒计时篇)相关的知识,希望对你有一定的参考价值。
自定义控件实现方式
本文中初步讨论了关于使用自定义控件实现倒计时的方式,原生控件设计仿获取验证码倒计时的实现以及注意细节
基于android Studio API 23开发
- 原生控件设计获取验证码倒计时
- 自定义控件的实现方式
- 自定义控件实现倒计时
- 结束语
原生控件设计获取验证码倒计时
需求前提:点击获取验证码即刻变为60s的倒计时并且不能点击,直到60s事件到了之后再次变为获取验证码
思想建树:
1. 需要一个计时器用于倒计时–》系统提供的计时器CountDownTimer 对象
2 控制计时器的倒计时过程以及结果
代码块
private CountDownTimer cdTimer = new CountDownTimer(60000, 1000)
@Override
public void onTick(long millis)
// 每过1000毫秒调用一次,millis为还剩多少毫秒
mGetCode.setText(getString(R.string.remaining,
(millis + 500) / 1000));
mGetCode.setClickable(false);
@Override
public void onFinish()
mGetCode.setText(R.string.sendValidateCode);
mGetCode.setClickable(true);
;
使用简单便捷,符合大部分的登录注册情况,效果图如下:
自定义控件的实现方式
了解实现自定义控件的基本方法和实现步骤:
1.首先要明确需要自定义的样式中所要在外界赋值的参数以及类型
<declare-styleable name="CountDownProgress">
<!-- 默认圆实心的颜色 -->
<attr name="default_circle_solide_color" format="color"/>
<!-- 默认圆边框的颜色 -->
<attr name="default_circle_stroke_color" format="color"/>
<!-- 默认圆边框的宽度 -->
<attr name="default_circle_stroke_width" format="dimension"/>
<!-- 默认圆的半径 -->
<attr name="default_circle_radius" format="dimension"/>
<!-- 进度条的颜色 -->
<attr name="progress_color" format="color"/>
<!-- 进度条的宽度 -->
<attr name="progress_width" format="dimension"/>
<!-- 小圆的实心颜色 -->
<attr name="small_circle_solide_color" format="color"/>
<!-- 小圆的边框颜色 -->
<attr name="small_circle_stroke_color" format="color"/>
<!-- 小圆的边框宽度 -->
<attr name="small_circle_stroke_width" format="dimension"/>
<!-- 小圆的半径 -->
<attr name="small_circle_radius" format="dimension"/>
<!-- 文字的颜色 -->
<attr name="text_color" format="color"/>
<!-- 文字的字体大小 -->
<attr name="text_size" format="dimension"/>
</declare-styleable>
其中会涉及到的format的类型:
1. reference:参考某一资源ID- - -drawable
2. color:颜色值- - -color
3. boolean:布尔值- - -boolean
4. dimension:尺寸值- - -dimension
5. float:浮点值- - -float
6. integer:整型值- - -integer
7. string:字符串- - -string
8. fraction:百分数- - -fraction
9. enum:枚举值- - -enum
10. flag:位或运算- - -flag
在完成自定义控件的书写情况,在XML中直接使用attrs中的属性的方式
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.sjxz.moji_weather.view.CountDownView
android:id="@+id/countdwonview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:paddingTop="35dp"
android:paddingRight="10dp"
android:paddingLeft="10dp"
app:text_color="@color/red"
/>
</RelativeLayout>
在这里需要注意的是要在配置 xmlns:app=”http://schemas.android.com/apk/res-auto”使得XML中识别到attrs中的参数属性,在自定义控件中直接使用XML文件定义的属性。如果不配置的情况typedArray.getIndexCount()就返回0。在XML中配置几个attrs中的参数属性,获取的数量就是多少。
2.了解自定义控件的绘制流程以及实现细节
每一个View/ViewGroup的显示都会经过三个过程:
1、measure过程(测量View显示的大小,位置);
2、layout过程(布局view的位置);- - -可按需求不去实现
3、draw过程(通过canvas绘制到界面上显示,形成了各色的View)
3.自定义控件代码实现步骤以及代码
首先自定义的View一定是继承View,并且有三个自己的构造函数。一般情况下,绘制的时候会走两个参数的构造函数(原因暂时不明),但是为了保险起见将三个构造函数全部初始化
需要将TypedArray初始化,并且将attrs中的数据全部定义用于当XML中没有使用任何参数的情况下,默认返回的数值,并且一定要调用 typedArray.recycle();因为在系统中TypedArray初始化后调用recycle主要是为了缓存。当recycle被调用后,这就说明这个对象从现在可以被重用了。TypedArray 内部持有部分数组,它们缓存在Resources类中的静态字段中,这样就不用每次使用前都需要分配内存。
在measure过程中确定自定义View的宽度和高度,这里需要普及一下其中使用的参数 MeasureSpec.AT_MOST- - -相当于我们设置为wrap_content||MeasureSpec.EXACTLY- - -相当于我们设置为match_parent或者为一个具体的值,最后一个UNSPECIFIED没有定义大小(一般情况下都不会用)
在ondraw方法中绘制相关的圆,使用animation监听动画效果并且设定时间动画
public class CountDownView extends View
private static int defaultCircleSolideColor = Color.BLUE;
private static int defaultCircleStrokeColor = Color.WHITE;//最底层的颜色
private static int defaultCircleStrokeWidth = 10;
private static int defaultCircleRadius = 60;
private static int progressColor = Color.GRAY;//进度条的颜色
private static int progressWidth = 11;//>defaultCircleStrokeWidth
private static int smallCircleSolideColor = Color.BLACK;
private static int smallCircleStrokeColor = Color.WHITE;
private static float smallCircleStrokeWidth = 8;
private static float smallCircleRadius = 30;
private static int textColor = Color.WHITE;//BLACK
private static float textSize = 30;
private static Paint defaultCriclePaint;
private static Paint progressPaint;
private static Paint textPaint;
private static float currentAngle;
public static String textDesc;
public static long countdownTime;
private static int mStartSweepValue = -90;
//设置画布圆形背景
private Paint backgroundPaint;
public static ValueAnimator animator;
public CountDownView(Context context)
super(context);
public CountDownView(Context context, AttributeSet attrs)
super(context, attrs);
initStyle(attrs);
setPaint();
public CountDownView(Context context, AttributeSet attrs, int defStyleAttr)
super(context, attrs, defStyleAttr);
//获取自定义属性
initStyle(attrs);
setPaint();
private void initStyle(AttributeSet attrs)
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CountDownProgress);
int indexCount = typedArray.getIndexCount();
for(int i=0;i<indexCount;i++)
int attr = typedArray.getIndex(i);
switch (attr)
case R.styleable.CountDownProgress_default_circle_solide_color:
defaultCircleSolideColor = typedArray.getColor(attr, defaultCircleSolideColor);
break;
case R.styleable.CountDownProgress_default_circle_stroke_color:
defaultCircleStrokeColor = typedArray.getColor(attr, defaultCircleStrokeColor);
break;
case R.styleable.CountDownProgress_default_circle_stroke_width:
defaultCircleStrokeWidth = (int) typedArray.getDimension(attr, defaultCircleStrokeWidth);
break;
case R.styleable.CountDownProgress_default_circle_radius:
defaultCircleRadius = (int) typedArray.getDimension(attr, defaultCircleRadius);
break;
case R.styleable.CountDownProgress_progress_color:
progressColor = typedArray.getColor(attr, progressColor);
break;
case R.styleable.CountDownProgress_progress_width:
progressWidth = (int) typedArray.getDimension(attr, progressWidth);
break;
case R.styleable.CountDownProgress_small_circle_solide_color:
smallCircleSolideColor = typedArray.getColor(attr, smallCircleSolideColor);
break;
case R.styleable.CountDownProgress_small_circle_stroke_color:
smallCircleStrokeColor = typedArray.getColor(attr, smallCircleStrokeColor);
break;
case R.styleable.CountDownProgress_small_circle_stroke_width:
smallCircleStrokeWidth = (int) typedArray.getDimension(attr, smallCircleStrokeWidth);
break;
case R.styleable.CountDownProgress_small_circle_radius:
smallCircleRadius = (int) typedArray.getDimension(attr, smallCircleRadius);
break;
case R.styleable.CountDownProgress_text_color:
textColor = typedArray.getColor(attr, textColor);
break;
case R.styleable.CountDownProgress_text_size:
textSize = (int) typedArray.getDimension(attr, textSize);
break;
typedArray.recycle();
private void setPaint()
//默认圆
defaultCriclePaint = new Paint();
defaultCriclePaint.setAntiAlias(true);//抗锯齿
defaultCriclePaint.setDither(true);//防抖动
defaultCriclePaint.setStyle(Paint.Style.STROKE);
defaultCriclePaint.setStrokeWidth(defaultCircleStrokeWidth);
defaultCriclePaint.setColor(defaultCircleStrokeColor);//这里先画边框的颜色,后续再添加画笔画实心的颜色
//默认圆上面的进度弧度
progressPaint = new Paint();
progressPaint.setAntiAlias(true);
progressPaint.setDither(true);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeWidth(progressWidth);
progressPaint.setColor(progressColor);
progressPaint.setStrokeCap(Paint.Cap.ROUND);//设置画笔笔刷样式
//文字画笔
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setDither(true);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setColor(textColor);
textPaint.setTextSize(textSize);
backgroundPaint=new Paint();
backgroundPaint.setAntiAlias(true);//抗锯齿
backgroundPaint.setDither(true);//防抖动
backgroundPaint.setStyle(Paint.Style.FILL);
backgroundPaint.setStrokeWidth(defaultCircleStrokeWidth);
backgroundPaint.setColor(progressColor);//这里先画边框的颜色,后续再添加画笔画实心的颜色
@Override
protected void onDraw(Canvas canvas)
super.onDraw(canvas);
canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
//背景圆
canvas.drawCircle(defaultCircleRadius, defaultCircleRadius, defaultCircleRadius, backgroundPaint);
//画默认圆
canvas.drawCircle(defaultCircleRadius, defaultCircleRadius, defaultCircleRadius, defaultCriclePaint);
//画进度圆弧
//currentAngle = getProgress()*1.0f/getMax()*360;recf适用于划出一块绘制的区域mStartSweepValue是开始的位置
canvas.drawArc(new RectF(0, 0, defaultCircleRadius*2, defaultCircleRadius*2),mStartSweepValue, 360*currentAngle,false,progressPaint);
//画中间文字
// String text = getProgress()+"%";
//获取文字的长度的方法
float textWidth = textPaint.measureText(textDesc);
float textHeight = (textPaint.descent() + textPaint.ascent()) / 2;
canvas.drawText(textDesc, defaultCircleRadius - textWidth/2, defaultCircleRadius - textHeight, textPaint);
canvas.restore();
/**
* 如果该View布局的宽高开发者没有精确的告诉,则需要进行测量,如果给出了精确的宽高则我们就不管了
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize;
int heightSize;
int strokeWidth = Math.max(defaultCircleStrokeWidth, progressWidth);
//精确指定宽高
if(widthMode != MeasureSpec.EXACTLY)
widthSize = getPaddingLeft() + defaultCircleRadius*2 + strokeWidth + getPaddingRight();
widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
if(heightMode != MeasureSpec.EXACTLY)
heightSize = getPaddingTop() + defaultCircleRadius*2 + strokeWidth + getPaddingBottom();
heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//属性动画
public void startCountDownTime(final OnCountdownFinishListener countdownFinishListener)
setClickable(true);
animator = ValueAnimator.ofFloat(0, 1.0f);
//动画时长,让进度条在CountDown时间内正好从0-360走完,这里由于用的是CountDownTimer定时器,倒计时要想减到0则总时长需要多加1000毫秒,所以这里时间也跟着+1000ms
animator.setDuration(countdownTime );//+ 1000
animator.setInterpolator(new LinearInterpolator());//匀速
animator.setRepeatCount(0);//表示不循环,-1表示无限循环
//值从0-1.0F 的动画,动画时长为countdownTime,ValueAnimator没有跟任何的控件相关联,那也正好说明ValueAnimator只是对值做动画运算,而不是针对控件的,我们需要监听ValueAnimator的动画过程来自己对控件做操作
//添加监听器,监听动画过程中值的实时变化(animation.getAnimatedValue()得到的值就是0-1.0)
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
@Override
public void onAnimationUpdate(ValueAnimator animation)
/**
* 这里我们已经知道ValueAnimator只是对值做动画运算,而不是针对控件的,因为我们设置的区间值为0-1.0f
* 所以animation.getAnimatedValue()得到的值也是在[0.0-1.0]区间,而我们在画进度条弧度时,设置的当前角度为360*currentAngle,
* 因此,当我们的区间值变为1.0的时候弧度刚好转了360度
*/
currentAngle = (float) animation.getAnimatedValue();
// Log.e("currentAngle",currentAngle+"");
invalidate();//实时刷新view,这样我们的进度条弧度就动起来了
);
//开启动画
animator.start();
//还需要另一个监听,监听动画状态的监听器
animator.addListener(new Animator.AnimatorListener()
@Override
public void onAnimationStart(Animator animation)
@Override
public void onAnimationEnd(Animator animation)
//倒计时结束的时候,需要通过自定义接口通知UI去处理其他业务逻辑
if(countdownFinishListener != null)
countdownFinishListener.countdownFinished();
if(countdownTime > 0)
setClickable(true);
else
setClickable(false);
@Override
public void onAnimationCancel(Animator animation)
@Override
public void onAnimationRepeat(Animator animation)
);
//调用倒计时操作
countdownMethod();
public CountDownTimer timer;
//倒计时的方法
private void countdownMethod()
timer= new CountDownTimer(countdownTime+1000, 1000)
@Override
public void onTick(long millisUntilFinished)
// Log.e("time",countdownTime+"");
countdownTime = countdownTime-1000;
textDesc = "跳过("+((countdownTime/1000)) + ")";
//countdownTime = countdownTime-1000;
//刷新view
invalidate();
@Override
public void onFinish()
//textDesc = 0 + "″";
//刷新view
// textDesc="跳过(0)";
// invalidate();
.start();
public void setCountdownTime(long countdownTime)
this.countdownTime = countdownTime;
textDesc = countdownTime / 1000 + "″";
public interface OnCountdownFinishListener
void countdownFinished();
4.显示当前界面
countdwonview.setCountdownTime(4 * 1000);
countdwonview.setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View v)
if(countdwonview.timer!=null)
if(countdwonview.animator!=null)
countdwonview.animator.cancel();
countdwonview.timer.cancel();
readyGoThenKill(MainActivity.class);
);
countdwonview.startCountDownTime(new CountDownView.OnCountdownFinishListener()
@Override
public void countdownFinished()
//动画结束后的操作
);
显示自定义的View倒计时大规模使用于splash页面的跳过,设计效果如图:
结束语
倒计时使用的范围很广,本文提出的注册倒计时,欢迎页面的倒计时样式已经算是对大部分项目都适用。在自定义倒计时的时候要特别的注意,因为大部分欢迎页面都有动画效果,如果在您的App中对动画进行监听结果跳转到首页,在这里就要取消倒计时控件中的点击跳转,因为在点击事件中需要添加清除动画效果,默认动画已经完成了,会先走动画监听的结果流程,相当于已经进行跳转了。由于代码量很少,并且大部分都已经加入了注释,相信是简单易懂的,本文也是在前人的基础上进行改造重绘界面,毕竟不能重复制造轮胎。如果想要进行自定义View的深入,我在github上提交了一个仿淘宝物流信息的自定义页面。链接:https://github.com/wyhnihaook/Logistics_information 。希望下载的同学帮我fork和star一下,有问题请多多指教,谢谢大家。
以上是关于自定义控件(倒计时篇)的主要内容,如果未能解决你的问题,请参考以下文章