Android自定义控件 倒计时

260 阅读8分钟
原文链接: mp.weixin.qq.com

点击上方蓝字关注公众号

码个蛋第273次推文

倒数5个数~

作者:容华谢后

博客:https://www.jianshu.com/u/34ece31cd6eb

文章目录

  • 写在前面

  • 实现

    • 初始化一些数据

    • 定义控件的大小

    • 绘制刻度盘

    • 绘制刻度盘

    • 绘制定时进度条

    • 绘制时间

    • 滑动事件

  • 写在最后

GitHub传送门(https://link.jianshu.com/?t=https://github.com/alidili/Demos/tree/master/CountdownDemo)

0

写在前面

本篇文章实现了一个简单的倒计时控件,主要运用了画布的操作,滑动角度计算等知识点,非常适合自定义控件的初学者进行学习,看下效果图:

1

实现

初始化一些数据

public class CountdownView extends View {    // 控件宽    private int width;    // 控件高    private int height;    // 刻度盘半径    private int dialRadius;    // 小时刻度高    private float hourScaleHeight = dp2px(6);    // 分钟刻度高    private float minuteScaleHeight = dp2px(4);    // 定时进度条宽    private float arcWidth = dp2px(6);    // 时间-分    private int time = 0;    // 刻度盘画笔    private Paint dialPaint;    // 时间画笔    private Paint timePaint;    // 是否移动    private boolean isMove;    // 当前旋转的角度    private float rotateAngle;    // 当前的角度    private float currentAngle;    // 时间改变监听    private OnCountdownListener onCountdownListener;    public CountdownView(Context context) {        this(context, null);    }    public CountdownView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public CountdownView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        // 刻度盘画笔        dialPaint = new Paint();        dialPaint.setAntiAlias(true);        dialPaint.setColor(Color.parseColor("#94C5FF"));        dialPaint.setStyle(Paint.Style.STROKE);        dialPaint.setStrokeCap(Paint.Cap.ROUND);        // 时间画笔        timePaint = new Paint();        timePaint.setAntiAlias(true);        timePaint.setColor(Color.parseColor("#94C5FF"));        timePaint.setTextSize(sp2px(33));        timePaint.setStyle(Paint.Style.STROKE);    }        ...}

定义控件的大小

@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {    super.onSizeChanged(w, h, oldw, oldh);    // 控件宽、高    width = height = Math.min(h, w);    // 刻度盘半径    dialRadius = (int) (width / 2 - dp2px(10));}

绘制刻度盘

/** * 绘制刻度盘 * * @param canvas 画布 */private void drawDial(Canvas canvas) {    // 绘制外层圆盘    dialPaint.setStrokeWidth(dp2px(2));    canvas.drawCircle(width / 2, height / 2, dialRadius, dialPaint);    // 将坐标原点移到控件中心    canvas.translate(getWidth() / 2, getHeight() / 2);    canvas.save();    // 绘制小时刻度    for (int i = 0; i < 12; i++) {        // 定时时间为0时正常绘制小时刻度        // 小时刻度没有被定时进度条覆盖时正常绘制小时刻度        if (time == 0 || i > time / 5) {            canvas.drawLine(0, -dialRadius, 0, -dialRadius + hourScaleHeight, dialPaint);        }        // 360 / 12 = 30;        canvas.rotate(30);    }    // 绘制分钟刻度    dialPaint.setStrokeWidth(dp2px(1));    for (int i = 0; i < 60; i++) {        // 小时刻度位置不绘制分钟刻度        // 分钟刻度没有被定时进度条覆盖时正常绘制分钟刻度        if (i % 5 != 0 && i > time) {            canvas.drawLine(0, -dialRadius, 0, -dialRadius + minuteScaleHeight, dialPaint);        }        // 360 / 60 = 6;        canvas.rotate(6);    }}

首先绘制一个圆,然后把坐标原点移动到控件中心,原点移动到控件中心后向上为负值,接着绘制小时刻度,一共有12个刻度,time的单位为分钟,要注意如果刻度被定时进度条覆盖就不再绘制,绘制分钟刻度同理,代码中已经写了很全的注释,不再多说,看下效果:

绘制定时进度条

/** * 绘制定时进度条 * * @param canvas 画布 */private void drawArc(Canvas canvas) {    if (time > 0) {        // 绘制起始标志        dialPaint.setStrokeWidth(dp2px(3));        canvas.drawLine(0, -dialRadius - hourScaleHeight, 0, -dialRadius + hourScaleHeight, dialPaint);        // 取消直线圆角设置        dialPaint.setStrokeCap(Paint.Cap.BUTT);        // 绘制圆弧        float arcWidth = dp2px(6);        for (int i = 0; i <= time * 6; i++) {            canvas.drawLine(0, -dialRadius - arcWidth / 2, 0, -dialRadius + arcWidth / 2, dialPaint);            // 最后一次不旋转画布            if (i != time * 6) {                canvas.rotate(1);            }        }        // 绘制结束标志        dialPaint.setStrokeCap(Paint.Cap.ROUND);        canvas.drawLine(0, -dialRadius - hourScaleHeight, 0, -dialRadius + hourScaleHeight, dialPaint);    }}

如果定时时间大于0则开始绘制定时进度条,重点说下绘制进度,在这里并没有使用绘制圆弧的方法,依然是通过旋转画布的方式绘制的,设置一个15分钟的进度,看下效果:

绘制时间

/** * 绘制时间 * * @param canvas 画布 */private void drawTime(Canvas canvas) {    canvas.restore();    String timeText = String.format(Locale.CHINA, "%02d", time) + " : 00";    // 获取时间的宽高    float timeWidth = timePaint.measureText(timeText);    float timeHeight = Math.abs(timePaint.ascent() + timePaint.descent());    // 居中显示    canvas.drawText(timeText, -timeWidth / 2, timeHeight / 2, timePaint);}

在控件中心绘制一段文本,重点在于如何获取文本的宽高,宽度直接测量就可以了,高度比较特殊,因为绘制的是数字,所以使用Math.abs(timePaint.ascent() + timePaint.descent());这种方式来获取文本高度,先挖个坑,下一篇文章详细讲一下文本的绘制,看下效果:

滑动事件

@Overridepublic boolean onTouchEvent(MotionEvent event) {    switch (event.getAction()) {        case MotionEvent.ACTION_DOWN:            // 按下的角度            currentAngle = calcAngle(event.getX(), event.getY());            break;        case MotionEvent.ACTION_MOVE:            // 标记正在移动            isMove = true;            // 移动的角度            float moveAngle = calcAngle(event.getX(), event.getY());            // 滑过的角度偏移量            float angleOffset = moveAngle - currentAngle;            // 防止越界            if (angleOffset < -270) {                angleOffset = angleOffset + 360;            } else if (angleOffset > 270) {                angleOffset = angleOffset - 360;            }            currentAngle = moveAngle;            // 计算时间            calcTime(angleOffset);            break;        case MotionEvent.ACTION_CANCEL:        case MotionEvent.ACTION_UP: {            if (isMove && onCountdownListener != null) {                // 回调倒计时改变方法                onCountdownListener.countdown(time);                isMove = false;            }            break;        }    }    return true;}

通过计算滑过的角度增量来设置当前的定时时间,看下如何来计算当前触摸点的角度:

前方高能,请减速慢行!

/** * 以刻度盘圆心为坐标圆点,建立坐标系,求出(targetX, targetY)坐标与x轴的夹角 * * @param targetX x坐标 * @param targetY y坐标 * @return (targetX, targetY)坐标与x轴的夹角 */private float calcAngle(float targetX, float targetY) {    // 以刻度盘圆心为坐标圆点    float x = targetX - width / 2;    float y = targetY - height / 2;    // 滑过的弧度    double radian;    if (x != 0) {        float tan = Math.abs(y / x);        if (x > 0) {            if (y >= 0) {                // 第四象限                radian = Math.atan(tan);            } else {                // 第一象限                radian = 2 * Math.PI - Math.atan(tan);            }        } else {            if (y >= 0) {                // 第三象限                radian = Math.PI - Math.atan(tan);            } else {                // 第二象限                radian = Math.PI + Math.atan(tan);            }        }    } else {        if (y > 0) {            // Y轴向下方向            radian = Math.PI / 2;        } else {            // Y轴向上方向            radian = Math.PI + Math.PI / 2;        }    }    // 完整圆的弧度为2π,角度为360度,所以180度等于π弧度    // 弧度 = 角度 / 180 * π    // 角度 = 弧度 / π * 180    return (float) (radian / Math.PI * 180);}

首先了解下弧度与角度的计算公式:

  • 完整圆的弧度为2π,角度为360度,所以180度等于π弧度

  • 弧度 = 角度 / 180 * π

  • 角度 = 弧度 / π * 180

然后以第一象限的点为例,计算一下触摸点的角度:

// 以刻度盘圆心为坐标圆点float x = targetX - width / 2;float y = targetY - height / 2;// 触摸点与x轴的夹角float tan = Math.abs(y / x);// 触摸点的弧度double radian = 2 * Math.PI - Math.atan(tan);// 触摸点的角度double angle = radian / Math.PI * 180;

看图理解:

根据滑过的角度计算当前的定时时间:

/** * 计算时间 * * @param angle 增加的角度 */private void calcTime(float angle) {    rotateAngle += angle;    if (rotateAngle < 0) {        rotateAngle = 0;    } else if (rotateAngle > 360) {        rotateAngle = 360;    }    time = (int) rotateAngle / 6;    invalidate();}

最后提供设置倒计时,和监听倒计时状态的方法:

/** * 设置倒计时 * * @param minute 分钟 */public void setCountdown(int minute) {    if (minute < 0 || minute > 60) {        return;    }    time = minute;    rotateAngle = minute * 6;    invalidate();}/** * 设置倒计时监听 * * @param onTempChangeListener 倒计时监听接口 */public void setOnCountdownListener(OnCountdownListener onCountdownListener) {    this.onCountdownListener = onCountdownListener;}/** * 倒计时监听接口 */public interface OnCountdownListener {    /**     * 倒计时     *     * @param temp 时间     */    void countdown(int time);}

大功告成,再看下效果:

2

写在最后

源码已经上传到GitHub上了,欢迎Fork,觉得还不错就Start一下吧!

GitHub传送门(https://link.jianshu.com/?t=https://github.com/alidili/Demos/tree/master/CountdownDemo)

点我下载本文Demo的Apk(https://link.jianshu.com/?t=https://github.com/alidili/Demos/raw/master/CountdownDemo/CountdownDemo.apk)