一、前言
时钟绘制有很多案例,但是也考验对坐标系和三角函数的理解。
时钟自定义作为Android 开发入门的经典案例,想必比少人都绘制过各种个样的时钟。从时钟的定义入门无非是最好的,因为在这种案例中,既可以学到文字绘制、线条的绘制、Canvas旋转应有尽有。
不过,这里的难点就要考验对初中和高中数学的熟悉程度,因为在这个过程中需要大量用到余弦函数和正弦函数,甚至还会用到反三角函数。
另外,我们也要熟悉坐标体系的变化,Canvas的任何变换方法都和矩阵有关,只不过官方封装了方法,但抵触逻辑依然是矩阵。
二、实现
2.1 预览效果
2.1.1 有边框的版本
2.1.2 有边框的版本
2.2 实现步骤
其实难点并不多,我们按照顺序和规则绘制即可,主要步骤如下
-
绘制时钟表盘
-
绘制刻度
-
绘制文本
-
绘制边框
当然,为了便于绘制,我们可以把中心点移动到View中心点,这样,View中心点就是坐标原点。
2.3 详细设计
基本参数设置,主要是绘制的一些工具和变量。
private static final float _1PX = 1; // 1px 偏移
private Paint strokePaint;
private float radius; //半径
private float borderSize = 20; //边框宽度
private boolean stopDraw = false; //tick控制
private boolean withOutline = false; //边框控制
2.4 文本时间格式化
我们要展示文本时间,当然还需要格式化时间,这里我们使用SimpleDateFormat,这里要补充一点,SimpleDateFormate线程不安全,如果有多线程绘制的需求一定要注意
final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
2.5 更新逻辑
更新逻辑我们使用View自带的方法,这里我们定义一个Runnable的任务去更新,主要解决两方面的问题,第一是防止非UI线程更新,这个很好理解。第二防止任务频率不稳定,如果在规定的时间内多了了任务,这个时候我们要删除原有的任务,类似debounce的设计。
Runnable invalidateTask = new Runnable() {
@Override
public void run() {
invalidate();
}
};
更新方法如下
private void startTicking() {
if (stopDraw) return;
removeCallbacks(invalidateTask);
postDelayed(invalidateTask, 500);
}
2.6 完整逻辑
下面是代码实现,不是很复杂,上手也很容易,所有的绘制基本上都是围绕着圆形展开,另外对于文本得绘制,千万不要旋转,不然9和6颠倒,效果是很奇怪的。
public class TickingClockView extends View {
private static final float _1PX = 1; // 1px 偏移
private Paint strokePaint;
private float radius; //半径
private float borderSize = 20; //边框宽度
private boolean stopDraw = false; //tick控制
private boolean withOutline = false; //边框控制
final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
public TickingClockView(Context context) {
this(context, null);
}
public TickingClockView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TickingClockView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
initCanvas(context);
}
private void initCanvas(Context c) {
strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
radius = Math.min(getWidth(), getHeight()) / 2f - borderSize;
long timeMillis = System.currentTimeMillis();
String format = sdf.format(timeMillis);
canvas.save();
canvas.translate(getWidth() / 2F, getHeight() / 2F);
//绘制表盘
strokePaint.setStyle(Paint.Style.FILL);
strokePaint.setColor(0xFFdddddd);
canvas.drawCircle(0, 0, radius - borderSize + _1PX, strokePaint);
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setTextSize(48);
canvas.rotate(0);
strokePaint.setStrokeCap(Paint.Cap.ROUND);
for (int i = 0; i < 12; i++) {
strokePaint.setColor(0xff333333);
strokePaint.setStrokeWidth(borderSize);
double radians = Math.toRadians(-i * 30);
float startX = (float) (Math.cos(radians) * (radius - borderSize * 3));
float startY = (float) (Math.sin(radians) * (radius - borderSize * 3));
float stopX = (float) (Math.cos(radians) * (radius - borderSize));
float stopY = (float) (Math.sin(radians) * (radius - borderSize));
String text = "" + (i + 1);
float textWidth = strokePaint.measureText(text);
int clockHourPosX = (int) ((Math.cos(Math.toRadians(i * 30 + 270 + 30)) * (radius - borderSize * 5)) - textWidth / 2);
int clockHourPosY = (int) ((Math.sin(Math.toRadians(i * 30 + 270 + 30)) * (radius - borderSize * 5)) + textWidth / 2);
canvas.drawLine(startX, startY, stopX, stopY, strokePaint);
strokePaint.setStrokeWidth(borderSize / 2F);
strokePaint.setColor(Color.BLACK);
strokePaint.setStyle(Paint.Style.FILL);
canvas.drawText(text, clockHourPosX, clockHourPosY, strokePaint);
for (int j = 1; j < 5; j++) {
double toRadians = Math.toRadians(-i * 30 - j * 6);
float minStartX = (float) (Math.cos(toRadians) * (radius - borderSize * 2));
float minStartY = (float) (Math.sin(toRadians) * (radius - borderSize * 2));
float minStopX = (float) (Math.cos(toRadians) * (radius - borderSize));
float minStopY = (float) (Math.sin(toRadians) * (radius - borderSize));
canvas.drawLine(minStartX, minStartY, minStopX, minStopY, strokePaint);
}
}
String[] strFormatTimes = format.split(":");
int secondText = Integer.parseInt(strFormatTimes[2]);
int minuteText = Integer.parseInt(strFormatTimes[1]);
int hourText = Integer.parseInt(strFormatTimes[0]) % 12;
strokePaint.setStyle(Paint.Style.STROKE);
//时针
double toRadians = Math.toRadians(-90 + hourText * 30 + 30 * minuteText / 60f + 30 * secondText / 3600f);
float startX = (int) (Math.cos(toRadians));
float startY = (int) (Math.sin(toRadians));
float stopX = (int) (Math.cos(toRadians) * (radius - borderSize * 12));
float stopY = (int) (Math.sin(toRadians) * (radius - borderSize * 12));
strokePaint.setColor(Color.BLACK);
strokePaint.setStrokeWidth(15);
canvas.drawLine(startX, startY, stopX, stopY, strokePaint);
strokePaint.setStrokeWidth(borderSize);
canvas.drawCircle(0, 0, borderSize, strokePaint);
//分针
double toRadians1 = Math.toRadians(-90 + minuteText * 6 + 6 * secondText / 60f);
startX = (int) (Math.cos(toRadians1) * (-30));
startY = (int) (Math.sin(toRadians1) * (-30));
stopX = (int) (Math.cos(toRadians1) * (radius - borderSize * 8));
stopY = (int) (Math.sin(toRadians1) * (radius - borderSize * 8));
strokePaint.setStrokeWidth(6);
strokePaint.setColor(Color.RED);
canvas.drawLine(startX, startY, stopX, stopY, strokePaint);
strokePaint.setStrokeWidth(borderSize / 2f);
canvas.drawCircle(0, 0, borderSize / 2f, strokePaint);
//秒针
double toRadians2 = Math.toRadians(-90 + 6 * secondText);
startX = (int) (Math.cos(toRadians2) * (-60));
startY = (int) (Math.sin(toRadians2) * (-60));
stopX = (int) (Math.cos(toRadians2) * (radius - borderSize * 6));
stopY = (int) (Math.sin(toRadians2) * (radius - borderSize * 6));
strokePaint.setStrokeWidth(4);
strokePaint.setColor(Color.DKGRAY);
canvas.drawLine(startX, startY, stopX, stopY, strokePaint);
strokePaint.setColor(Color.CYAN);
strokePaint.setStrokeWidth(borderSize / 4);
canvas.drawCircle(0, 0, borderSize / 4, strokePaint);
strokePaint.setStrokeWidth(1);
strokePaint.setStyle(Paint.Style.STROKE.FILL);
strokePaint.setTextSize(60f);
strokePaint.setColor(Color.BLACK);
float measureText = strokePaint.measureText(format);
canvas.drawText(format, -measureText / 2, radius / 2f, strokePaint);
if (withOutline) {
//绘制裱框
strokePaint.setColor(0xFFe5f9ef);
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setStrokeWidth(borderSize);
canvas.drawCircle(0, 0, radius - borderSize / 2, strokePaint);
}
canvas.restore();
startTicking();
}
Runnable invalidateTask = new Runnable() {
@Override
public void run() {
invalidate();
}
};
private void startTicking() {
if (stopDraw) return;
removeCallbacks(invalidateTask);
postDelayed(invalidateTask, 500);
}
public void setWithOutline(boolean withOutline) {
this.withOutline = withOutline;
postInvalidate();
}
public boolean isWithOutline() {
return withOutline;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopDraw = true;
}
}
三、总结
绘制时钟案例是非常好的入门教程,因此很多时候,如果要学习Canvas绘制,简单的案例索然乏味,甚至没有成就感,但是时钟相比来说,会让你感觉的很多收获。
刻度表是非常入门级的,有的博客上说这是程序员进阶的级别,实际上夸大其词。
本篇到这里结束了,希望本篇内容对你有所帮助。