Android自定义控件 倒计时

GitHub传送门

1.写在前面

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

倒计时

2.实现

初始化一些数据

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);
    }

    ...
}

定义控件的大小

@Override
protected 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());这种方式来获取文本高度,先挖个坑,下一篇文章详细讲一下文本的绘制,看下效果:

绘制时间

滑动事件

@Override
public 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);
}

大功告成,再看下效果:

倒计时

3.写在最后

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

GitHub传送门

点我下载本文Demo的Apk

相关推荐
封面 1 序 2 捐助说明 5 目 录 7 第一章 View的绘图流程 12 1.1、概述 12 1.2、Activity的组成结构 13 1.3、View树的绘图流程 15 1.3.1 测量组件大小 16 1.3.2 确定子组件的位置 17 1.3.3 绘制组件 18 1.4、说点别的 22 1.5 练习作业 22 第二章 Graphics2D API 23 2.1、概述 23 2.2、Point类和PointF类 23 2.3、Rect类和RectF类 25 2.4、Bitmap类和BitmapDrawable类 32 2.5、Canvas类与Paint类 34 2.5.1 绘图概述 34 2.5.2 Paint类 34 2.5.3 Canvas类 39 2.6 练习作业 63 第三章 使用Graphics2D实现动态效果 64 3.1 概述 64 3.2 invalidate()方法 65 3.3 坐标转换 69 3.4 剪切区(Clip) 73 3.5 案例:指针走动的手表 82 3.6 练习作业 88 第四章 双缓存技术 89 4.1 双缓存 89 4.2 在屏幕上绘制曲线 90 4.3 在屏幕上绘制矩形 99 4.4 案例:绘图App 104 4.4.1 绘图属性 106 4.4.2 软件参数 108 4.4.3 绘图缓冲区 109 4.4.4 撤消操作 111 4.4.5 图形绘制 113 4.4.6 绘图区 118 4.4.7 主界面 119 4.5 练习作业 122 第五章 阴影、渐变和位图运算 123 5.1 概述 123 5.2 阴影 123 5.3 渐变 125 5.3.1 线性渐变(LinearGradient) 126 5.3.2 径向渐变(RadialGradient) 130 5.3.3 扫描渐变(SweepGradient) 135 5.3.4 位图渐变(BitmapShader) 138 5.3.5 混合渐变(ComposeShader) 140 5.3.6 渐变与Matrix 142 5.4 位图运算 143 5.4.1 PorterDuffXfermode 143 5.4.2 图层(Layer) 146 5.4.3 位图运算技巧 148 5.5 案例1:圆形头像 152 5.6 案例2:刮刮乐 156 5.7 练习作业 161 第六章 自定义组件 163 6.1 概述 163 6.2 自定义组件的基本结构 164 6.3 重写onMeasure方法 166 6.4 组件属性 175 6.4.1 属性的基本定义 175 6.4.2 读取来自style和theme中的属性 181 6.5 案例1:圆形ImageView组件 186 6.6 案例2:验证码组件CodeView 190 6.7 练习作业 202 第七章 自定义容器 204 7.1 概述 204 7.2 ViewGroup类 205 7.2.1 ViewGroup常用方法 205 7.2.2 ViewGroup的工作原理 208 7.2.3 重写onLayout()方法 213 7.3 CornerLayout布局 217 7.3.1 基本实现 217 7.3.2 内边距padding 224 7.3.3 外边距margin 228 7.3.4 自定义LayoutParams 238 7.4 案例:流式布局(FlowLayout) 246 7.5 练习作业 256 第八章 Scroller与平滑滚动 257 8.1 概述 257 8.2 认识scrollTo()和scrollBy()方法 258 8.3 Scroller类 264 8.4 平滑滚动的工作原理 271 8.5 案例:触摸滑屏 272 8.5.1 触摸滑屏的技术分析 272 8.5.2 速度跟踪器VelocityTracker 273 8.5.3 触摸滑屏的分步实现 274 8.6 练习作业 285 第九章 侧边栏 287 9.1 概述 287 9.2 使用二进制保存标识数据 289 9.2.1 位运算符 289 9.2.2 位运算的常用功能 292 9.3 继承自ViewGroup的侧边栏 293 9.4 继承自HorizontalScrollView的侧边栏 304 9.5 练习作业 312 第十章 加强版ListView 313 10.1 概述 313 10.2 ListView的基本使用 314 10.3 ListItem随手指左右滑动 318 10.4 向右滑动删除ListItem 326 10.5 滑动ListItem出现删除按钮 336 10.5.1 列表项专用容器ExtendLayout 337 10.5.2 列表
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页