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); } ...}复制代码
定义控件的大小
@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);}复制代码
大功告成,再看下效果:
3.写在最后
源码已经上传到GitHub上了,欢迎Fork,觉得还不错就Start一下吧!