Android自定义View之带小圆圈的倒计时圆形进度条

请尊重个人劳动成果,转载注明出处,谢谢!
http://blog.csdn.net/xiaxiazaizai01/article/details/52415377

上一篇写了一个可随时暂停的圆形进度条,接下来再来撸一个带小圆圈的倒计时View,主要难点是对于随着进度条变化而变化的小圆的绘制。看了givemeacondom大神写的小圆的绘制,大神是通过小圆运动在第一象限、第二象限等不同象限内的四种不同情况来绘制的,说实话,,数学忘的差不多了,好多公式着实是看不懂,再加上原作者注释的又很少,看的花都谢了。。。最后还是放弃了,这里非常感谢群里的yissan大神,他给我提供了一个思路,他说根据进度的变化算出小圆的x、y坐标的变化,于是乎,我又拾起了课本,温习了一下弧度、正弦sinα、余弦cosα,从而巧妙的将小圆绘制粗来了。在这里向yissan小伙伴表示感谢。也非常感谢givemeacondom大神给出的创意,我在作者的基础上,通过自己的想法简化了复杂的坐标计算。喜欢原文的可以点击givemeacondom,本文中我会把注释写的详细些,大家可以画画图配合着理解,因为。。代码和图更配哦,废话不多说,老规矩,先来一张效果图。
Android自定义View之带小圆圈的倒计时圆形进度条_第1张图片

接下来我们就按着自定义View的五步走,实现上图的效果。什么??你不知道哪五步,好吧,那我就引用下yissan小伙伴博客中提到的五步走。

根据Android Developers官网的介绍,自定义控件你需要以下的步骤。(根据你的需要,某些步骤可以省略)

1、创建View

2、处理View的布局

3、绘制View

4、与用户进行交互

5、优化已定义的View

辣么,接下来我们就开始一步步实现这个效果了。

1、创建View

(1)自定义view属性,我们在res/values下面新建一个attr.xml文件,设置我们的自定义view属性


<resources>
    <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>
resources>

(2)在我们的自定义View类中去获取这些属性

public CountDownProgress(Context context) {
        this(context,null);
    }

    public CountDownProgress(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CountDownProgress(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CountDownProgress);
        int indexCount = typedArray.getIndexCount();
        for(int i=0;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对象
        typedArray.recycle();
        //设置画笔
        setPaint();
    }

设置画笔的方法,new画笔的操作不要在onDraw()方法中进行

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);//设置画笔笔刷样式
        //进度上面的小圆
        smallCirclePaint = new Paint();
        smallCirclePaint.setAntiAlias(true);
        smallCirclePaint.setDither(true);
        smallCirclePaint.setStyle(Paint.Style.STROKE);
        smallCirclePaint.setStrokeWidth(smallCircleStrokeWidth);
        smallCirclePaint.setColor(smallCircleStrokeColor);
           //画进度上面的小圆的实心画笔(主要是将小圆的实心颜色设置成白色)
        smallCircleSolidePaint = new Paint();
        smallCircleSolidePaint.setAntiAlias(true);
        smallCircleSolidePaint.setDither(true);
        smallCircleSolidePaint.setStyle(Paint.Style.FILL);
        smallCircleSolidePaint.setColor(smallCircleSolideColor);

        //文字画笔
        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setDither(true);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setColor(textColor);
        textPaint.setTextSize(textSize);
    }

2、处理View的布局(也就是测量onMeasure)

/**
     * 如果该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);
    }

3、绘制View,即onDraw()

这里为了能让大家看的更明白,我粗略的画了个坐标图,这里我们以手机左上角为坐标原点,大圆的圆心坐标为(r,r),而对于小圆的运动轨迹,你也可以以大圆的圆心(r,r)为坐标原点进行分析,这里我仍是以左上角(0,0)为坐标原点,那么小圆在几个特殊点的左边,在图中我已经标出来了,为什么要标记小圆运动到这几个特殊点的坐标,这是因为小圆是随着进度条的运动而运动的,我们要通过这些坐标计算分析得出小圆的圆心坐标的变化规律。后面会说到。

Android自定义View之带小圆圈的倒计时圆形进度条_第2张图片
首先我们还是来一步步实现,首先我们先不考虑小圆,实现一个中间带文字进度变化的圆形进度条,如下图所示
Android自定义View之带小圆圈的倒计时圆形进度条_第3张图片
辣么,接下来是代码展示了,为了方便进度计算,我们让我们的自定义view继承ProgressBar,而ProgressBar带有getProgress()、getMax()方法,从而可以计算出最外层的进度条圆弧扫过的角度currentAngle = getProgress()*1.0f/getMax()*360

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        canvas.translate(getPaddingLeft(), getPaddingTop());
        //画默认圆
        canvas.drawCircle(defaultCircleRadius,defaultCircleRadius,defaultCircleRadius,defaultCriclePaint);

        //画进度圆弧
        currentAngle = getProgress()*1.0f/getMax()*360;
        canvas.drawArc(new RectF(0,0,defaultCircleRadius*2,defaultCircleRadius*2),mStartSweepValue, currentAngle ,false,progressPaint);
        //画中间文字
        String text = getProgress()+"%";
        //获取文字的长度的方法
        float textWidth = textPaint.measureText(text );
        float textHeight = (textPaint.descent() + textPaint.ascent()) / 2;
        canvas.drawText(text, defaultCircleRadius-textWidth/2, defaultCircleRadius-textHeight, textPaint);

        canvas.restore();

    }

接下来是让进度条圆弧以及中间的文字动起来(这已经属于第四步,与用户进行交互)

public class MainActivity extends AppCompatActivity {

    private CountDownProgress countDownProgress;
    private int progress;

    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case HANDLER_MESSAGE:
                    progress = countDownProgress.getProgress();
                    countDownProgress.setProgress(++progress);
                    if(progress >= 100){
                        handler.removeMessages(HANDLER_MESSAGE);
                        progress = 0;
                        countDownProgress.setProgress(0);
                    }else{
                        handler.sendEmptyMessageDelayed(HANDLER_MESSAGE, 100);
                    }
                    break;
            }
        }
    };
    public static final int HANDLER_MESSAGE = 2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        countDownProgress = (CountDownProgress) findViewById(R.id.countdownProgress);
        countDownProgress.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Message message = Message.obtain();
                message.what = HANDLER_MESSAGE;
                handler.sendMessage(message);
            }
        });
    }
}

接下来我们实现带小圆的绘制,我们知道由正余弦可以得出 X = cosα * r (r:半径),Y = sinα * r ,以及 弧度 = 度 * π / 180,而π在Android中用Math.PI表示,再根据上面我们画的坐标图中小圆运动到图中几个特殊点的坐标可以得出小圆的X、Y坐标的规律:X = sinα * r + r,Y = r - cosα * r,按照此规律就不难算出小圆的坐标变化了。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        canvas.translate(getPaddingLeft(), getPaddingTop());
        //画默认圆
        canvas.drawCircle(defaultCircleRadius,defaultCircleRadius,defaultCircleRadius,defaultCriclePaint);

        //画进度圆弧
        //currentAngle = getProgress()*1.0f/getMax()*360;
        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);

        //画小圆
        float currentDegreeFlag = 360*currentAngle + extraDistance;
        float smallCircleX = 0,smallCircleY = 0;
        float hudu = (float) Math.abs(Math.PI * currentDegreeFlag / 180);//Math.abs:绝对值 ,Math.PI:表示π , 弧度 = 度*π / 180
        smallCircleX = (float) Math.abs(Math.sin(hudu) * defaultCircleRadius + defaultCircleRadius);
        smallCircleY = (float) Math.abs(defaultCircleRadius -Math.cos(hudu) * defaultCircleRadius);
        canvas.drawCircle(smallCircleX, smallCircleY, smallCircleRadius, smallCirclePaint);
        canvas.drawCircle(smallCircleX, smallCircleY, smallCircleRadius - smallCircleStrokeWidth, smallCircleSolidePaint);//画小圆的实心

        canvas.restore();

    }

上面说了,如果我们的自定义view继承的不是ProgressBar,则ProgressBar的一些方法我们就用不了了,这里我们直接继承View,辣么,进度条圆弧扫过的角度我们可以用属性动画来实现。注释在代码中相当详细,这回你可以秒懂了吧。。

//属性动画
    public void startCountDownTime(final OnCountdownFinishListener countdownFinishListener){
        setClickable(false);
        ValueAnimator 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();
    }

实现倒计时,我们这里用Android系统提供的CountDownTimer实现,下面简单介绍下CountDownTimer的使用,第一个参数是总时间,第二个是每隔多长时间执行一次onTick方法,注意,这两个参数值都是以毫秒为单位。在测试的时候发现用CountDownTimer时,倒计时不能到0的情况,下面贴出CountDownTimer的部分源码,查看源码发现,当mMillisInFuture = 0的时候直接执行了onFinish方法,大家可以调试的时候查看log打印日志

public synchronized final CountDownTimer start() {
        mCancelled = false;
        if (mMillisInFuture <= 0) {
            onFinish();
            return this;
        }
        mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
        mHandler.sendMessage(mHandler.obtainMessage(MSG));
        return this;
    }

下面把倒计时的代码贴出来

//倒计时的方法
    private void countdownMethod(){
        new CountDownTimer(countdownTime+1000, 1000) {
            @Override
            public void onTick(long millisUntilFinished) {
       //         Log.e("time",countdownTime+"");
                countdownTime = countdownTime-1000;
                textDesc = countdownTime/1000 + "″";
                //countdownTime = countdownTime-1000;
                Log.e("time",countdownTime+"");
                //刷新view
                invalidate();
            }
            @Override
            public void onFinish() {
                //textDesc = 0 + "″";
                textDesc = "时间到";
                //同时隐藏小球
                smallCirclePaint.setColor(getResources().getColor(android.R.color.transparent));
                smallCircleSolidePaint.setColor(getResources().getColor(android.R.color.transparent));
                //刷新view
                invalidate();
            }
        }.start();
    }

对于希望从什么时间开始倒计时,我们交给开发者自己去决定,所以这里我们提供个供外界设置倒计时总时间的方法

public void setCountdownTime(long countdownTime){
        this.countdownTime = countdownTime;
        textDesc = countdownTime / 1000 + "″";
    }

当倒计时结束后,我们需要提供个接口去告诉UI,下面该你处理一些逻辑了

public interface OnCountdownFinishListener{
        void countdownFinished();
    }

最后,再看下我们的布局文件以及MainActivity如何使用


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <com.example.customcountdownprogress.CountDownProgress
        android:id="@+id/countdownProgress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="20dp"
        />
RelativeLayout>

MainActivity

public class MainActivity extends AppCompatActivity {

    private CountDownProgress countDownProgress;
    private int progress;

    /*private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case HANDLER_MESSAGE:
                    progress = countDownProgress.getProgress();
                    countDownProgress.setProgress(++progress);
                    if(progress >= 100){
                        handler.removeMessages(HANDLER_MESSAGE);
                        progress = 0;
                        countDownProgress.setProgress(0);
                    }else{
                        handler.sendEmptyMessageDelayed(HANDLER_MESSAGE, 100);
                    }
                    break;
            }
        }
    };
    public static final int HANDLER_MESSAGE = 2;*/

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        countDownProgress = (CountDownProgress) findViewById(R.id.countdownProgress);
        countDownProgress.setCountdownTime(10*1000);
        countDownProgress.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                countDownProgress.startCountDownTime(new CountDownProgress.OnCountdownFinishListener() {
                    @Override
                    public void countdownFinished() {
                        Toast.makeText(MainActivity.this, "倒计时结束了--->该UI处理界面逻辑了", Toast.LENGTH_LONG).show();
                    }
                });
                /*Message message = Message.obtain();
                message.what = HANDLER_MESSAGE;
                handler.sendMessage(message);*/
            }
        });
    }
}

万事开头难,要敢于尝试,慢慢积累。虽然代码中我已经注释的相当相当详细了,但是如果你有问题的话,欢迎留言。如果对你有所帮助的话,别忘了动动你的小手顶一下哈

点击下载源码

你可能感兴趣的:(Android,Android自定义View)