自定义控件(倒计时篇)

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.自定义控件代码实现步骤以及代码

  1. 首先自定义的View一定是继承View,并且有三个自己的构造函数。一般情况下,绘制的时候会走两个参数的构造函数(原因暂时不明),但是为了保险起见将三个构造函数全部初始化

  2. 需要将TypedArray初始化,并且将attrs中的数据全部定义用于当XML中没有使用任何参数的情况下,默认返回的数值,并且一定要调用 typedArray.recycle();因为在系统中TypedArray初始化后调用recycle主要是为了缓存。当recycle被调用后,这就说明这个对象从现在可以被重用了。TypedArray 内部持有部分数组,它们缓存在Resources类中的静态字段中,这样就不用每次使用前都需要分配内存。

  3. 在measure过程中确定自定义View的宽度和高度,这里需要普及一下其中使用的参数 MeasureSpec.AT_MOST- - -相当于我们设置为wrap_content||MeasureSpec.EXACTLY- - -相当于我们设置为match_parent或者为一个具体的值,最后一个UNSPECIFIED没有定义大小(一般情况下都不会用)

  4. 在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一下,有问题请多多指教,谢谢大家。

以上是关于自定义控件(倒计时篇)的主要内容,如果未能解决你的问题,请参考以下文章

Android自定义控件篇 圆形倒计时

自定义控件(仿PopupWindow篇)

自定义控件(仿PopupWindow篇)

自定义控件(Dialog篇)

自定义控件(Dialog篇)

自定义控件(倒计时)