自定义表盘
之前看过一个大神得文章,也是自定义精美表盘的博客,正好自己在自定义View这一块比较薄弱,所以决定用自己得方法来实现一个表盘样式,来锻炼自己:
实现这个表盘大体需要如下几个步骤:
- 绘制外层圆形
- 绘制表盘的刻度
- 绘制表盘上的时间
- 绘制时分秒的指针
- 绘制指针交叉点
- 计算当前时间,重新绘制
首先介绍一下相关的属性
/**
* 设置画布中心点
*/
private float centerX, centerY;
/**
* 画笔
*/
private Paint mPaint;
/**
* 时分秒 指针的颜色
*/
private int mHourColor, mMinuteColor, mSecondColor;
/**
* 时分秒 指针的宽度
*/
private float mHourWidth, mMinuteWidth, mSecondWidth;
/**
* 字体的大小
*/
private float mTextSize;
/**
* 字体的颜色
*/
private int mTextColor;
/**
* 圆形的半径
*/
private float mRadius;
/**
* 刻度的颜色
*/
private int scaleBoldColor, scaleColor;
/**
* 刻度的宽度
*/
private float scaleBoldWidth, scaleWidth;
/**
* 两个刻度之间的角度
*/
private float minDegree;
/**
* 两个整点刻度之间的角度
*/
private float maxDegree;
/**
* 时分秒指针转过的角度
*/
private float hourDegree, minutDegree, secondDegree;
以及相关属性的值的获取:
当然此方法都用在构造方法中。对自定义View构造方法不理解的同学可以自行查阅资料。
接下来就具体讲解一下绘制过程
首先贴上会使用的方法:dp转化为pix
/**
* dp转px
*/
public static int dp2px(Context ctx, float dp) {
float density = ctx.getResources().getDisplayMetrics().density;
int px = (int) (dp * density + 0.5f);// 4.9->5 4.4->4
return px;
}
绘制之前我们当然还要计算相关的属性,比如半径,画布的中心点:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = w / 2;//中心点横坐标
centerY = h / 2;//中心点纵坐标
//半径取宽和高的最小值的一半,再留出一点距离
mRadius = Math.min(w, h) / 2 - dp2px(getContext(), 10);
}
1. 绘制外部圆环
绘制一个圆当然是手到擒来呀!
/**
* 圆
*
* @param canvas
*/
private void drawCircle(Canvas canvas) {
canvas.save();
Path path = new Path();
path.addCircle(0, 0, mRadius, Path.Direction.CW);
mPaint.setStrokeWidth(dp2px(getContext(), 1));
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(path, mPaint);
canvas.restore();
}
效果图
canvas.save() 和 canvas.restore() 方法,我个人的理解就是用来保存和恢复当前绘制阶段canvas的状态,使得不同的绘制内容都有对应的canvas的状态,以不受其他阶段的影响 ; 至于Path,它可以绘制线,圆,矩形,椭圆,圆角矩形等内容,至于它的使用,我也是在一个大神的自定义View系列博客中一步一步学习的,写的是相当不错,你敢戳一下吗?,保证你满载而归 !!!
绘制表盘刻度
先上代码块:
/**
* 刻度
*
* @param canvas
*/
private void drawScale(Canvas canvas)
{
canvas.save();
mPaint.setStyle(Paint.Style.FILL);
//60个刻度
for (int i = 0; i < 60; i++)
{
if (i % 5 == 0)
{//整点的刻度
mPaint.setColor(scaleBoldColor);
mPaint.setStrokeWidth(scaleBoldWidth);
canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 20), mPaint);
}
else
{
mPaint.setColor(scaleColor);
mPaint.setStrokeWidth(scaleWidth);
canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 15), mPaint);
}
//画完刻度之后再旋转画布
canvas.rotate(minDegree);
}
canvas.restore();
}
效果图
首先设置画笔的颜色,填充方式等。表盘上有60个刻度,我们不可能一个一去绘制的,所以要在for循环里面去完成,从效果图可以看到整点对应的刻度都是加深的颜色,并且比普通的刻度稍微长一点,因此我们要分两种情况去绘制了。首先,和数字对其的刻度:我们也称之为整点刻度,我们使用 (i % 5 == 0)来做判断依据,当余数为0的时候正好指针指着整点刻度,此时我们就会只一条线,
canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 20), mPaint);
前四个值是,线条的起点坐标和结束坐标,第五个就是我们的画笔。我们先绘制最正上方的那个线条,起点坐标(0, mRadius - dp2px(getContext(), 5)),终点坐标(0, mRadius - dp2px(getContext(), 20)),我们是用当前的半径减去一个固定值而已,这个很容易理解!再者,我们要绘制两个整点刻度之间的刻度,不管是整点刻度还是普通的刻度,它们之间的角度都是一样的 即 :minDegree = 360 / 60 ,我们称之为小角度,当然后面还会讲到一个大角度的。
canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 15), mPaint);
同绘制整点刻度是一个道理。结束纵坐标只不过要比上一种稍微大一点,
最后再进行画布的旋转操作;
//画完刻度之后再旋转画布
canvas.rotate(minDegree);
到此刻度已经绘制完成
绘制数字
代码:
/**
* 字 drawText 第二个参数的意思默认是字符串的左边在屏幕的位置,第三个是指定这个字符baseline在屏幕上的位置
*
* @param canvas
*/
private void drawText(Canvas canvas) {
canvas.save();
String[] text = new String[]{"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"};
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);
mPaint.setStrokeWidth(dp2px(getContext(), 0.5f));
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setTextAlign(Paint.Align.CENTER);
float degree12 = -90;//初始角度
double radian;//弧度
for (int i = 0; i < 12; i++) {
Rect textBounds = new Rect();
mPaint.getTextBounds(text[i], 0, text[i].length(), textBounds);
radian = 2 * Math.PI / 360 * degree12;//角度转化为弧度
canvas.drawText(text[i], (float) ((mRadius - dp2px(getContext(), 30)) * Math.cos(radian)),(float) ((mRadius - dp2px(getContext(), 30)) * Math.sin(radian)) + dp2px(getContext(), 5), mPaint);
degree12 += maxDegree;
}
canvas.restore();
}
效果:
同样我们先设置画笔,同时设置字体的位置绘制矩形的中心,Paint.Align.CENTER 这个属性比计算快多了 ,不过不是很准确,关于自定义View绘制字体请自行查阅资料。
在drawText的时候,我是先大概计算一下数字位置的坐标,具体计算过程看图:
我们假设最正上方的当前角度为-90度,如图所示位置的角度则为30度,字体的位置坐标大概就死内圆和直线的交点处。计算公式则为:内圆半径 x 当前的角度的正余弦值。
//横坐标
((mRadius - dp2px(getContext(), 30)) * Math.cos(radian))
由于纵坐标有些偏差 所以偷懒了一下 在纵坐标上加了一个固定值:
(float) ((mRadius - dp2px(getContext(), 30)) * Math.sin(radian)) + dp2px(getContext(), 5)
在循环到下一个位置的时候,角度要加上30度。在这里要注意的是:Math.cos() 和 Math.sin() 需要填入的是弧度,所以我们在绘制之前先将对应的角度转化为弧度了,公式:弧度 = 2 * Math.PI / 360 * 角度。这样就顺利完成了。
绘制指针
代码:
/**
* 指针
*
* @param canvas
*/
private void drawClockPoint(Canvas canvas) {
caculateDegree();
//绘制时针
canvas.save();
canvas.rotate(hourDegree);
mPaint.setColor(mHourColor);
mPaint.setStrokeWidth(mHourWidth);
RectF hourRectF = new RectF(-mHourWidth / 2, -mRadius + dp2px(getContext(), 80),
mHourWidth / 2, mRadius / 9);
canvas.drawRoundRect(hourRectF, 10, 10, mPaint);
canvas.restore();
//绘制分针
canvas.save();
canvas.rotate(minutDegree);
mPaint.setColor(mMinuteColor);
mPaint.setStrokeWidth(mMinuteWidth);
RectF minuteRectF = new RectF(-mMinuteWidth / 2, -mRadius + dp2px(getContext(), 50),
mMinuteWidth / 2, mRadius / 9);
canvas.drawRoundRect(minuteRectF, 10, 10, mPaint);
canvas.restore();
//绘制秒针
canvas.save();
canvas.rotate(secondDegree);
mPaint.setColor(mSecondColor);
mPaint.setStrokeWidth(mSecondWidth);
RectF secondRectF = new RectF(-mSecondWidth / 2, -mRadius + dp2px(getContext(), 30),
mSecondWidth / 2, mRadius / 9);
canvas.drawRoundRect(secondRectF, 10, 10, mPaint);
canvas.restore();
mPaint.setColor(mSecondColor);
canvas.drawCircle(0, 0, dp2px(getContext(), 8), mPaint);
}
效果图:
在绘制指针的时候,使用了canvas绘制圆角矩形的方法,首先要设置的是rectf的四个值,这个也可以理解为当前圆角矩形的左上角和右下角的坐标值,由于我们将坐标轴移至了中心点,坐标计算起来还是特别方便的,默认会的圆角矩形都是指向12点方向,这样就很容易理解了。
绘制中心点
这个很容易理解啦,就是绘制一个圆,要求和秒针的颜色是一样的 ,这样看起来比较美观,我随意设置了一个半径;
canvas.drawCircle(0, 0, dp2px(getContext(), 8), mPaint);
计算当前时间
代码:
/**
* 计算当前时分秒指针的角度
*/
private void caculateDegree() {
Calendar mCanendar = Calendar.getInstance();
int hour = mCanendar.get(Calendar.HOUR_OF_DAY);//24小时制 ,HOUR的话是12小时制
int minute = mCanendar.get(Calendar.MINUTE);
int second = mCanendar.get(Calendar.SECOND);
hourDegree = (hour % 24) * 360 / 12;
minutDegree = minute * 360 / 60;
secondDegree = second * 360 / 60;
}
这里我们区分了12小时制和24小时制,并计算出当前时间时分秒指针应该在的正确角度。不过没有完善的是,秒针在走动的时候,分针和时针是不动的,这个和事实是不同的,这里暂时就不计算了。
完整代码
所有方法在ondraw方法中调用啦,不过还多了一句就是:
postInvalidateDelayed(1000);
在这里模拟秒针转动,每隔一秒重新绘制一次。
完整的自定义代码如下:
/**
* 设置画布中心点
*/
private float centerX, centerY;
/**
* 画笔
*/
private Paint mPaint;
/**
* 时分秒 指针的颜色
*/
private int mHourColor, mMinuteColor, mSecondColor;
/**
* 时分秒 指针的宽度
*/
private float mHourWidth, mMinuteWidth, mSecondWidth;
/**
* 字体的大小
*/
private float mTextSize;
/**
* 字体的颜色
*/
private int mTextColor;
/**
* 圆形的半径
*/
private float mRadius;
/**
* 刻度的颜色
*/
private int scaleBoldColor, scaleColor;
/**
* 刻度的宽度
*/
private float scaleBoldWidth, scaleWidth;
/**
* 两个刻度之间的角度
*/
private float minDegree;
/**
* 两个整点刻度之间的角度
*/
private float maxDegree;
/**
* 时分秒指针转过的角度
*/
private float hourDegree, minutDegree, secondDegree;
public CustomClock(Context context) {
super(context);
initView(context, null);
}
public CustomClock(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context, attrs);
}
public CustomClock(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs);
}
private void initView(Context context, AttributeSet attrs) {
//获取自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomClock);
mHourColor = typedArray.getColor(R.styleable.CustomClock_hourPointColor, Color.BLACK);
mHourWidth = typedArray.getDimension(R.styleable.CustomClock_hourPointWidth, dp2px(context, 3f));
mMinuteColor = typedArray.getColor(R.styleable.CustomClock_minutePointColor, Color.BLUE);
mMinuteWidth = typedArray.getDimension(R.styleable.CustomClock_minutePointWidth, dp2px(context, 2f));
mSecondWidth = typedArray.getDimension(R.styleable.CustomClock_secondPointWidth, dp2px(context, 1f));
mSecondColor = typedArray.getColor(R.styleable.CustomClock_secondPointColor, Color.RED);
scaleWidth = typedArray.getDimension(R.styleable.CustomClock_scaleWidth, dp2px(context, 1f));
scaleColor = typedArray.getColor(R.styleable.CustomClock_scaleColor, Color.parseColor("#858585"));
scaleBoldColor = typedArray.getColor(R.styleable.CustomClock_scaleBoldColor, Color.parseColor("#040304"));
scaleBoldWidth = typedArray.getDimension(R.styleable.CustomClock_scaleBoldWidth, dp2px(context, 1.5f));
mTextColor = typedArray.getColor(R.styleable.CustomClock_textColor, Color.parseColor("#141214"));
mTextSize = typedArray.getDimension(R.styleable.CustomClock_textSize, dp2px(context, 14));
//回收typedArray对象
typedArray.recycle();
minDegree = 6;
maxDegree = 30;
initPaint();
}
private void initPaint() {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setAntiAlias(true);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = w / 2;
centerY = h / 2;
mRadius = Math.min(w, h) / 2 - dp2px(getContext(), 10);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
//将画布原点移至view中心点
canvas.translate(centerX, centerY);
//绘制圆形
drawCircle(canvas);
//绘制刻度
drawScale(canvas);
//绘制字体
drawText(canvas);
//绘制时分秒指针
drawClockPoint(canvas);
canvas.restore();
//延迟一秒刷新
postInvalidateDelayed(1000);
}
/**
* 圆
*
* @param canvas
*/
private void drawCircle(Canvas canvas) {
canvas.save();
Path path = new Path();
path.addCircle(0, 0, mRadius, Path.Direction.CW);
mPaint.setStrokeWidth(dp2px(getContext(), 1));
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(path, mPaint);
canvas.restore();
}
/**
* 刻度
*
* @param canvas
*/
private void drawScale(Canvas canvas) {
canvas.save();
mPaint.setStyle(Paint.Style.FILL);
//60个刻度
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) {//整点的刻度
mPaint.setColor(scaleBoldColor);
mPaint.setStrokeWidth(scaleBoldWidth);
canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 20), mPaint);
} else {
mPaint.setColor(scaleColor);
mPaint.setStrokeWidth(scaleWidth);
canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 15), mPaint);
}
//画完刻度之后再旋转画布
canvas.rotate(minDegree);
}
canvas.restore();
}
/**
* 字 drawText 第二个参数的意思默认是字符串的左边在屏幕的位置,第三个是指定这个字符baseline在屏幕上的位置
*
* @param canvas
*/
private void drawText(Canvas canvas) {
canvas.save();
String[] text = new String[]{"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"};
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);
mPaint.setStrokeWidth(dp2px(getContext(), 0.5f));
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setTextAlign(Paint.Align.CENTER);
float degree12 = -90;//初始角度
double radian;//弧度
for (int i = 0; i < 12; i++) {
Rect textBounds = new Rect();
mPaint.getTextBounds(text[i], 0, text[i].length(), textBounds);
radian = 2 * Math.PI / 360 * degree12;//角度转化为弧度
canvas.drawText(text[i], (float) ((mRadius - dp2px(getContext(), 30)) * Math.cos(radian)),
(float) ((mRadius - dp2px(getContext(), 30)) * Math.sin(radian)) + dp2px(getContext(), 5), mPaint);
degree12 += maxDegree;
}
canvas.restore();
}
/**
* 指针
*
* @param canvas
*/
private void drawClockPoint(Canvas canvas) {
caculateDegree();
//绘制时针
canvas.save();
canvas.rotate(hourDegree);
mPaint.setColor(mHourColor);
mPaint.setStrokeWidth(mHourWidth);
RectF hourRectF = new RectF(-mHourWidth / 2, -mRadius + dp2px(getContext(), 80),
mHourWidth / 2, mRadius / 9);
canvas.drawRoundRect(hourRectF, 10, 10, mPaint);
canvas.restore();
//绘制分针
canvas.save();
canvas.rotate(minutDegree);
mPaint.setColor(mMinuteColor);
mPaint.setStrokeWidth(mMinuteWidth);
RectF minuteRectF = new RectF(-mMinuteWidth / 2, -mRadius + dp2px(getContext(), 50),
mMinuteWidth / 2, mRadius / 9);
canvas.drawRoundRect(minuteRectF, 10, 10, mPaint);
canvas.restore();
//绘制秒针
canvas.save();
canvas.rotate(secondDegree);
mPaint.setColor(mSecondColor);
mPaint.setStrokeWidth(mSecondWidth);
RectF secondRectF = new RectF(-mSecondWidth / 2, -mRadius + dp2px(getContext(), 30),
mSecondWidth / 2, mRadius / 9);
canvas.drawRoundRect(secondRectF, 10, 10, mPaint);
canvas.restore();
mPaint.setColor(mSecondColor);
canvas.drawCircle(0, 0, dp2px(getContext(), 8), mPaint);
}
/**
* 计算当前时分秒指针的角度
*/
private void caculateDegree() {
Calendar mCanendar = Calendar.getInstance();
int hour = mCanendar.get(Calendar.HOUR_OF_DAY);//24小时制 ,HOUR的话是12小时制
int minute = mCanendar.get(Calendar.MINUTE);
int second = mCanendar.get(Calendar.SECOND);
hourDegree = (hour % 24) * 360 / 12;
minutDegree = minute * 360 / 60;
secondDegree = second * 360 / 60;
}
/**
* dp转px
*/
public static int dp2px(Context ctx, float dp) {
float density = ctx.getResources().getDisplayMetrics().density;
int px = (int) (dp * density + 0.5f);// 4.9->5 4.4->4
return px;
}
到此:整个自定义表盘就绘制完成了,由于自定义能力不足,所以绘制的时候,也查阅了不少资料,虽然看着很简单,但是自己动手实现起来会有这样那样的问题,总之一句话:基础不牢,任重而道远哪!如有错误,请指正。谢谢!