封面
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一下吧!