Android自定义View之Canvas绘制基本图形(二)-- 自定义时钟
前言
前面一篇主要是巩固Cavas绘制基本图形(如直线,矩形,点等等),今天同样是复习Cavas画圆,圆弧,等等,但是今天会多了一个path,以及Canvas画布的旋转、缩放、平移等等,画布的保存(save)和回滚(restore),接下来进入我们的主题。
先来一张静态图片给大家看一下,我复习完这一节课程之后所做的一个东西,随后再上gif图。
看完了这个,接下来我们就要去先掌握做这个东西的一些基本的知识点。
绘制基本图形
首先,在自定义View中初始化画笔
private void initPaint() { paint = new Paint(); paint.setColor(Color.RED); paint.setStrokeWidth(3); paint.setStyle(Paint.Style.STROKE); path = new Path(); }
接下来我们就可以在 void onDraw(Canvas canvas) 画各种图形了。
画一个半径为100的空心圆和一个半径为100的实心圆(改一下style即可)
canvas.drawCircle(100, 100, 100, paint); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(400, 100, 100, paint);
画一段圆弧和一个扇形

对比图中左右两个圆弧,你会发现,当我们useCenter即是否使用中心为false时候,画出来的圆弧就是起始角度点和结束角度点两点之间的连线构成的弧形,而为true的时候,画出来的弧形则是起始角度点和结束角度点分别与中心点的连线构成的图形,即一个扇形。
paint.setStyle(Paint.Style.FILL);
//绘制弧线区域 RectF rect = new RectF(0, 0, 100, 100); canvas.drawArc(rect, //弧线所使用的矩形区域大小 0, //开始角度 90, //扫过的角度 false, //是否使用中心 paint); //绘制弧线区域 RectF rect2 = new RectF(100, 0, 200, 100); canvas.drawArc(rect2, //弧线所使用的矩形区域大小 0, //开始角度 90, //扫过的角度 true, //是否使用中心 paint);
对比图中左右两个圆弧,你会发现,当我们useCenter即是否使用中心为false时候,画出来的圆弧就是起始角度点和结束角度点两点之间的连线构成的弧形,而为true的时候,画出来的弧形则是起始角度点和结束角度点分别与中心点的连线构成的图形,即一个扇形。
画一个椭圆drawOval(oval, paint);
//定义一个矩形区域 RectF oval = new RectF(0,0,300,200); //矩形区域内切椭圆 canvas.drawOval(oval, paint);
按照指定坐标点绘制文字
//按照既定点 绘制文本内容 paint.setTextSize(24); String textString = "转动的是指针,逝去的是年华,不变的是真心。"; float textPoint[] = new float[]{ 20,20, //第一个字坐标20,20 40,40, //第二个字坐标40,40 60,60, 80,80, 100,100, 120,120, 140,140, 160,140, 180,140, 200,140, 220,140, 240,140, 260,140, 280,140, 300,160, 320,180, 340,200, 360,220, 380,240, 400,260, 420,280}; canvas.drawPosText(textString, textPoint, paint);
使用Path画一条路径
//将起始点移动到 坐标150,150
path.moveTo(150, 150); path.lineTo(450, 150); // 画线 path.lineTo(200,300); path.lineTo(300, 50); path.lineTo(400, 300); path.lineTo(150,150); canvas.drawPath(path, paint);
使用drawTextOnPath根据path绘制一串文字
String text = "任时光匆匆流去,我只在乎你。若时间是一个圆,与你在圆的另一头重逢。"; paint.setTextSize(20); //定义一条路径,将起始点移动到坐标100,100 path.moveTo(100, 100); path.lineTo(150, 200); path.lineTo(200,250); path.lineTo(400,250); path.lineTo(450,200); path.lineTo(500,100); path.lineTo(650,100); canvas.drawTextOnPath(text, path, 20, 20, paint);
好了,基本的图形绘制算是基本过了一遍了,那么接下来就应该搞事情了,曾经看到很多网上的博客都自定义画了一个时钟,于是我也想尝试一下,画一个属于自己的style的时钟,叫“奥迪牌手表”,开始撸起袖子干活了。
时钟实例
首先,我们自定义一个View叫ClockView继承自View,然后在ClockView的构造方法中我们需要初始化一个画笔,这里我是在Activity中直接通过new一个对象的方式来调用ClockView的,所以我是在一个参数的构造函数中初始化一个画笔的,如果是在xml文件中调用则需要在两个参数中调用,初始化如下public ClockView(Context context) { super(context); mCalendar = Calendar.getInstance(); // 初始化一个画笔 paint = new Paint(); paint.setColor(Color.BLUE); paint.setStrokeJoin(Paint.Join.ROUND); // 圆形 paint.setStrokeCap(Paint.Cap.ROUND); // 圆角 paint.setStrokeWidth(3); // 通知handler重绘 handler.sendEmptyMessageDelayed(INVALIDATE_SEND_MESSAGE_TAG,1000); }
初始化画笔之后接下来我们需要做的就是将画布的起始坐标点移动一下,移动到画布宽度的中心,高的话就距离顶部200处,然后画手表圆圈,如下
paint.setAntiAlias(true); //抗锯齿 paint.setStyle(Paint.Style.STROKE); canvas.translate(canvas.getWidth() / 2, 200); // 将位置移动画纸的坐标点 canvas.drawCircle(0,0,100,paint); // 画圆圈
画好圆圈之后,为了手表不那么单调,,于是我要在手表圈内沿着弧度圆绘制一句话“Every second should create something new in it”,由于我们前面已经绘制好了一个圆形,我们是要在这个基础上添加东西的,所以需要将前面画好东西的画布保存下来,调用方法canvas.save()即可,代码如下
// 使用path绘制路径文字
canvas.save(); // 保存画布状态 canvas.translate(-75,-75);// 将圆心点的坐便往左往上移动,并将该点作为起始点(0,0) Path path = new Path(); // 初始化一条路径 path.addArc(new RectF(0,0,150,150),-224,268); // 画圆弧 -224起始角度,268为扫过的角度 //canvas.drawRect(new RectF(0,0,150,150),paint); Paint citePaint = new Paint(paint); // 使用前一个画笔的样式 citePaint.setTextSize(14); citePaint.setStrokeWidth(1); canvas.drawTextOnPath("Every second should create something new in it", path, 28, 0, citePaint); canvas.restore();下面就到了画手表圆圈上的刻度了,刻度有两种,一种是长一点的小时刻度,另一种则是短一点的分钟刻度,具体思路是:一个时钟总的刻度数是60(小时刻度和分钟刻度总和),因此我们需要将这个i = 60作为一个循环条件,按顺时针方向逐个刻度画,画的时候是从12点钟方向开始,每隔5个刻度( i % 5 == 0)画一根时钟刻度的长线,并将小时数字绘制到该刻度的旁边,然后在两个小时长刻度线之间画4根短刻度线,到这里我是不是已经完成了我们画刻度的操作捏?然而并没有,你会发现,每个刻度都会画在同一个位置,即12点钟的方向,因此在这里我们必须要注意的就是,每画完一个刻度就要将画布旋转全面已经画过的刻度所占的角度,即canvas.rotate( 360 / count , 0f , 0f ),三个参数分别表示:旋转角度、x轴偏移量、y轴偏移量。代码如下:
// 画时钟刻度尺子 Paint tmpPaint = new Paint(paint); tmpPaint.setStrokeWidth(1); float y = 100; // 圆的半径那么长 int count = 60; // 总刻度数 for (int i = count; i > 0; i--) { // 如果i从0开始是顺时针从y坐标正方向开始画,如果i从count开始则是从坐标负方向开始画起 if (i == count) // 标志从哪里开始画,目的是便于直观观察 paint.setColor(Color.RED); else paint.setColor(Color.BLUE); if (i % 5 == 0){ canvas.drawLine(0f,-y,0f,-y-12f,paint); // 画长刻度线,线长12f,起点(0f,y) 终点(0f,y + 12f) if (i == count) // 画 12 点 canvas.drawText(String.valueOf(i / 5), -4f, -y-25f, tmpPaint); // 文字,绘制原点x坐标,绘制原点y坐标,画笔 else // 画 1 ~ 11 点 canvas.drawText(String.valueOf(12 - i / 5), -4f, -y-25f, tmpPaint); // 文字,绘制原点x坐标,绘制原点y坐标,画笔 }else { canvas.drawLine(0f,-y,0f,-y-5f,tmpPaint); // 画短刻度线 } canvas.rotate(360 / count,0f,0f); // 旋转画布 旋转角度,x倾斜,y倾斜度 }下一步,继续画手表的圆心,也就是画两个半径大小不一样的重叠的实心圆圈作为旋转中心
// 绘制中心点
tmpPaint.setColor(Color.BLACK); tmpPaint.setStrokeWidth(4); canvas.drawCircle(0,0,10,tmpPaint); tmpPaint.setStyle(Paint.Style.FILL); tmpPaint.setColor(Color.RED); canvas.drawCircle(0,0,5,tmpPaint);
基本的框架已经差不多画完了,接下来就是开始让时针、分针、秒针都动起来了,因此,我们需要做的就是将三根指针画出来,然后获取当前的系统时间,并将当前时间的小时、分钟、秒钟对应扫过的角度计算出来,并将这个角度作为画布的旋转角度,即可画出这三个指针所在手表上的位置,代码如下:
// 获取当前系统时间
int minute = mCalendar.get(Calendar.MINUTE);//得到当前分钟数 int hour = mCalendar.get(Calendar.HOUR);//得到当前小时数 int sec = mCalendar.get(Calendar.SECOND);//得到当前秒数 // 绘制秒针 paint.setColor(Color.GREEN); paint.setStrokeWidth(1); //canvas.drawLine(0, 15, 0, -85, paint); float secDegree = sec/60f*360;//得到秒针旋转的角度 canvas.save(); canvas.rotate(secDegree,0,0); canvas.drawLine(0,15,0,-85,paint); canvas.restore(); // 绘制分针 paint.setColor(Color.BLUE); paint.setStrokeWidth(2); //canvas.drawLine(0, 15, 0, -65, paint); float minuteDegree = minute/60f*360;//得到分针旋转的角度 canvas.save(); canvas.rotate(minuteDegree, 0, 0); canvas.drawLine(0,15, 0, -65, paint); canvas.restore(); // 绘制时针 paint.setColor(Color.RED); paint.setStrokeWidth(3); //canvas.drawLine(0, 15, 0, -35, paint); float hourDegree = (hour*60+minute)/12f/60*360;//得到时钟旋转的角度 canvas.save(); canvas.rotate(hourDegree, 0, 0); canvas.drawLine(0, 15, 0, -35, paint); canvas.restore(); // 重绘,复原画笔为初始化的颜色, paint.setColor(Color.BLUE);
有些手表还会有日期和星期,因此我也尝试着把他画上去了,同样的也是要先获取当前的系统日期,然后计算当天是星期几,然后格式化一下就可以了,如下代码:
// 绘制日期
int year = mCalendar.get(Calendar.YEAR); int month = mCalendar.get(Calendar.MONTH) + 1; // +1是因为Java中月份是从0开始的 int day = mCalendar.get(Calendar.DAY_OF_MONTH);
int dayOfWeek = mCalendar.get(Calendar.DAY_OF_WEEK); String weekStr = "星期"; switch (dayOfWeek){ case 1: weekStr = weekStr + "日"; break; case 2: weekStr = weekStr + "一"; break; case 3: weekStr = weekStr + "二"; break; case 4: weekStr = weekStr + "三"; break; case 5: weekStr = weekStr + "四"; break; case 6: weekStr = weekStr + "五"; break; case 7: weekStr = weekStr + "六"; break; } RectF rectF = new RectF(-65,25,15,45); // 日期 RectF rectFWeek = new RectF(25,25,65,45); // 星期 Paint datePaint = new Paint(); datePaint.setColor(Color.BLACK); datePaint.setStyle(Paint.Style.FILL); datePaint.setAntiAlias(true); datePaint.setStrokeWidth(1); datePaint.setTextSize(10); // 日期框 canvas.drawRect(rectF,paint); canvas.drawText(year+"年"+month+"月"+day+"日",-60,38,datePaint); // 星期框 datePaint.setColor(Color.RED); canvas.drawRect(rectFWeek,paint); canvas.drawText(weekStr,30,38,datePaint);
说好的是装逼的“奥迪牌手表”,不画几个圈圈上去怎么装逼
// 画奥迪标志 datePaint.setStyle(Paint.Style.STROKE); datePaint.setStrokeWidth(2); canvas.drawCircle(-20,-40,10,datePaint); canvas.drawCircle(-5,-40,10,datePaint); canvas.drawCircle(10,-40,10,datePaint); canvas.drawCircle(25,-40,10,datePaint);
至此,一个静态的时钟表已经可以画出来了,但是我们需要让它的三个时针都动起来,因此我们用到了handler,一启动即在初始化的时候就要发一个空消息去通知UI线程重新绘制,然后重绘完之后继续延时1000毫秒发一条空消息进行下一次秒的绘制,集体的handler,代码如下
// 每隔一秒,在handler中调用一次重绘方法 private Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { switch (msg.what){ case INVALIDATE_SEND_MESSAGE_TAG: mCalendar = Calendar.getInstance(); invalidate(); // 告诉Ui线程重新绘制 handler.sendEmptyMessageDelayed(INVALIDATE_SEND_MESSAGE_TAG,1000);// 间隔1秒发送空消息 break; default: break; } } };
在构造函数中即初始化画笔的时候也要先获取一个Calendar的实例对象,以及向UI线程发送一条空消息
mCalendar = Calendar.getInstance(); // 通知handler重绘 handler.sendEmptyMessageDelayed(INVALIDATE_SEND_MESSAGE_TAG,1000);
好了,奥迪牌手表已经全部完成了,而且三根指针都动起来了,附上ClockView.java完整代码如下:
gif效果图如下:
package com.chen.canvas;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import java.util.Calendar;
/**
* Created by Administrator on 2018/4/24 0024.
*/
public class ClockView extends View {
private Paint paint;
private Calendar mCalendar; // 获取一个时间对象
public static final int INVALIDATE_SEND_MESSAGE_TAG = 0X01;
String word1 ="转动的是指针,逝去的是年华,不变的是真心。";
String word2 = "任时光匆匆流去,我只在乎你。";
String word3 = "若时间是 一个圆,";
String word4 = "与你在圆的另一头重逢。";
// 任时光匆匆流去我只在乎你。
// 若时间是一个圆,
// 与你在圆的另一头重逢
// 每隔一秒,在handler中调用一次重绘方法
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case INVALIDATE_SEND_MESSAGE_TAG:
mCalendar = Calendar.getInstance();
invalidate(); // 告诉Ui线程重新绘制
handler.sendEmptyMessageDelayed(INVALIDATE_SEND_MESSAGE_TAG,1000);// 间隔1秒发送空消息
break;
default:
break;
}
}
};
public ClockView(Context context) {
super(context);
mCalendar = Calendar.getInstance();
// 初始化一个画笔
paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStrokeJoin(Paint.Join.ROUND); // 圆形
paint.setStrokeCap(Paint.Cap.ROUND); // 圆角
paint.setStrokeWidth(3);
// 通知handler重绘
handler.sendEmptyMessageDelayed(INVALIDATE_SEND_MESSAGE_TAG,1000);
}
public ClockView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public ClockView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
int start=0;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setAntiAlias(true); //抗锯齿
paint.setStyle(Paint.Style.STROKE);
canvas.translate(canvas.getWidth() / 2, 200); // 将位置移动画纸的坐标点
canvas.drawCircle(0,0,100,paint); // 画圆圈
// 使用path绘制路径文字
canvas.save(); // 保存画布状态
canvas.translate(-75,-75);// 将圆心点的坐便往左往上移动,并将该点作为起始点(0,0)
Path path = new Path(); // 初始化一条路径
path.addArc(new RectF(0,0,150,150),-224,268); // 画圆弧 -224起始角度,268为扫过的角度
// canvas.drawRect(new RectF(0,0,150,150),paint);
Paint citePaint = new Paint(paint); // 使用前一个画笔的样式
citePaint.setTextSize(14);
citePaint.setStrokeWidth(1);
canvas.drawTextOnPath("Every second should create something new in it", path, 28, 0, citePaint);
canvas.restore();
// 画时钟刻度尺子
Paint tmpPaint = new Paint(paint);
tmpPaint.setStrokeWidth(1);
float y = 100; // 圆的半径那么长
int count = 60; // 总刻度数
for (int i = count; i > 0; i--) { // 如果i从0开始是顺时针从y坐标正方向开始画,如果i从count开始则是从坐标负方向开始画起
if (i == count) // 标志从哪里开始画,目的是便于直观观察
paint.setColor(Color.RED);
else
paint.setColor(Color.BLUE);
if (i % 5 == 0){
canvas.drawLine(0f,-y,0f,-y-12f,paint); // 画长刻度线,线长12f,起点(0f,y) 终点(0f,y + 12f)
if (i == count) // 画 12 点
canvas.drawText(String.valueOf(i / 5), -4f, -y-25f, tmpPaint); // 文字,绘制原点x坐标,绘制原点y坐标,画笔
else // 画 1 ~ 11 点
canvas.drawText(String.valueOf(12 - i / 5), -4f, -y-25f, tmpPaint); // 文字,绘制原点x坐标,绘制原点y坐标,画笔
}else {
canvas.drawLine(0f,-y,0f,-y-5f,tmpPaint); // 画短刻度线
}
canvas.rotate(360 / count,0f,0f); // 旋转画布 旋转角度,x倾斜,y倾斜度
}
// 绘制中心点
tmpPaint.setColor(Color.BLACK);
tmpPaint.setStrokeWidth(4);
canvas.drawCircle(0,0,10,tmpPaint);
tmpPaint.setStyle(Paint.Style.FILL);
tmpPaint.setColor(Color.RED);
canvas.drawCircle(0,0,5,tmpPaint);
// 获取当前系统时间
int minute = mCalendar.get(Calendar.MINUTE);//得到当前分钟数
int hour = mCalendar.get(Calendar.HOUR);//得到当前小时数
int sec = mCalendar.get(Calendar.SECOND);//得到当前秒数
// 绘制秒针
paint.setColor(Color.GREEN);
paint.setStrokeWidth(1);
// canvas.drawLine(0, 15, 0, -85, paint);
float secDegree = sec/60f*360;//得到秒针旋转的角度
canvas.save();
canvas.rotate(secDegree,0,0);
canvas.drawLine(0,15,0,-85,paint);
canvas.restore();
// 绘制分针
paint.setColor(Color.BLUE);
paint.setStrokeWidth(2);
// canvas.drawLine(0, 15, 0, -65, paint);
float minuteDegree = minute/60f*360;//得到分针旋转的角度
canvas.save();
canvas.rotate(minuteDegree, 0, 0);
canvas.drawLine(0,15, 0, -65, paint);
canvas.restore();
// 绘制时针
paint.setColor(Color.RED);
paint.setStrokeWidth(3);
// canvas.drawLine(0, 15, 0, -35, paint);
float hourDegree = (hour*60+minute)/12f/60*360;//得到时钟旋转的角度
canvas.save();
canvas.rotate(hourDegree, 0, 0);
canvas.drawLine(0, 15, 0, -35, paint);
canvas.restore();
// 重绘,复原画笔为初始化的颜色,
paint.setColor(Color.BLUE);
// 绘制日期
int year = mCalendar.get(Calendar.YEAR);
int month = mCalendar.get(Calendar.MONTH) + 1; // +1是因为Java中月份是从0开始的
int day = mCalendar.get(Calendar.DAY_OF_MONTH);
int dayOfWeek = mCalendar.get(Calendar.DAY_OF_WEEK);
String weekStr = "星期";
switch (dayOfWeek){
case 1:
weekStr = weekStr + "日";
break;
case 2:
weekStr = weekStr + "一";
break;
case 3:
weekStr = weekStr + "二";
break;
case 4:
weekStr = weekStr + "三";
break;
case 5:
weekStr = weekStr + "四";
break;
case 6:
weekStr = weekStr + "五";
break;
case 7:
weekStr = weekStr + "六";
break;
}
RectF rectF = new RectF(-65,25,15,45); // 日期
RectF rectFWeek = new RectF(25,25,65,45); // 星期
Paint datePaint = new Paint();
datePaint.setColor(Color.BLACK);
datePaint.setStyle(Paint.Style.FILL);
datePaint.setAntiAlias(true);
datePaint.setStrokeWidth(1);
datePaint.setTextSize(10);
// 日期框
canvas.drawRect(rectF,paint);
canvas.drawText(year+"年"+month+"月"+day+"日",-60,38,datePaint);
// 星期框
datePaint.setColor(Color.RED);
canvas.drawRect(rectFWeek,paint);
canvas.drawText(weekStr,30,38,datePaint);
// 画奥迪标志
datePaint.setStyle(Paint.Style.STROKE);
datePaint.setStrokeWidth(2);
canvas.drawCircle(-20,-40,10,datePaint);
canvas.drawCircle(-5,-40,10,datePaint);
canvas.drawCircle(10,-40,10,datePaint);
canvas.drawCircle(25,-40,10,datePaint);
// 画第一行字
Path wordPath1 = new Path();
wordPath1.moveTo(-300,200);
wordPath1.lineTo(300,200);
wordPath1.lineTo(-300,200);
datePaint.setTextSize(28);
canvas.drawTextOnPath(word1,wordPath1,15,15,datePaint);
// 画第二行字
Path wordPath2 = new Path();
wordPath2.addArc(new RectF(-200,250,200,450),-160,230);
datePaint.setTextSize(28);
datePaint.setColor(Color.GREEN);
canvas.drawTextOnPath(word2,wordPath2,15,15,datePaint);
// 画第三行字
Path wordPath3 = new Path();
wordPath3.moveTo(-150,350);
wordPath3.lineTo(150,350);
datePaint.setTextSize(28);
datePaint.setColor(Color.BLUE);
canvas.drawTextOnPath(word3,wordPath3,15,15,datePaint);
// 画第4行字
Path wordPath4 = new Path();
wordPath4.moveTo(-100,500);
wordPath4.addCircle(0,500,100, Path.Direction.CW);
datePaint.setTextSize(28);
datePaint.setColor(Color.CYAN);
canvas.drawTextOnPath(word4,wordPath4,15,15,datePaint);
// 画一个加载转动圈圈
Paint loadPaint=new Paint();
loadPaint.setStyle(Paint.Style.STROKE);
loadPaint.setColor(Color.BLACK);
loadPaint.setStrokeWidth(2);
loadPaint.setAntiAlias(true);
RectF oval=new RectF(-30,
400,
30,
460);
canvas.drawArc(oval, start=start>360?0:start+3, start, false, loadPaint);
loadPaint.setTextSize(18);
if (start % 3 == 0)
canvas.drawText(String.valueOf(start / 3) + "% ",-15,430,loadPaint); // 画百分比例
invalidate();
}
}
然后,在Activity中调用:
package com.chen.canvas;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private PieView pieView;
private ArrayList<PieData> mData = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new ClockView(this));
}
}
另外,你会发现ClockView.java后面会多出一部分代码,上面都有注释的,就是画上图的手表底下的那些文字以及加载转动圈的代码,其实就是做完再次巩固一下而已,懒得重新写一个工程了,所以我直接丢到这里去了,影响不大。