View
继承View
,实现构造方法,如下
public WatchBoard(Context context) {
this(context, null);
}
public WatchBoard(Context context, AttributeSet attrs) {
super(context, attrs);
}
private float mRadius; //外圆半径
private float mPadding; //边距
private float mTextSize; //文字大小
private float mHourPointWidth; //时针宽度
private float mMinutePointWidth; //分针宽度
private float mSecondPointWidth; //秒针宽度
private int mPointRadius; // 指针圆角
private float mPointEndLength; //指针末尾的长度
private int mColorLong; //长线的颜色
private int mColorShort; //短线的颜色
private int mHourPointColor; //时针的颜色
private int mMinutePointColor; //分针的颜色
private int mSecondPointColor; //秒针的颜色
private Paint mPaint; //画笔
value
文件下新建watch_board_attr.xml
文件,内容如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="WatchBoard">
<!--表盘的边距-->
<attr name="wb_padding" format="dimension"/>
<!--表盘文字大小-->
<attr name="wb_text_size" format="dimension"/>
<!--时针的宽度-->
<attr name="wb_hour_pointer_width" format="dimension"/>
<!--分针的宽度-->
<attr name="wb_minute_pointer_width" format="dimension"/>
<!--秒针的宽度-->
<attr name="wb_second_pointer_width" format="dimension"/>
<!--指针圆角值-->
<attr name="wb_pointer_corner_radius" format="dimension"/>
<!--指针超过中心点的长度-->
<attr name="wb_pointer_end_length" format="dimension"/>
<!--时刻刻度颜色-->
<attr name="wb_scale_long_color" format="color"/>
<!--非时刻刻度颜色-->
<attr name="wb_scale_short_color" format="color"/>
<!--时针颜色-->
<attr name="wb_hour_pointer_color" format="color"/>
<!--分针颜色-->
<attr name="wb_minute_pointer_color" format="color"/>
<!--秒针颜色-->
<attr name="wb_second_pointer_color" format="color"/>
</declare-styleable>
</resources>
public WatchBoard(Context context, AttributeSet attrs) {
super(context, attrs);
obtainStyledAttrs(attrs); //获取自定义的属性
}
private void obtainStyledAttrs(AttributeSet attrs) {
TypedArray array = null;
try {
array = getContext().obtainStyledAttributes(attrs, R.styleable.WatchBoard);
mPadding = array.getDimension(R.styleable.WatchBoard_wb_padding, DptoPx(10));
mTextSize = array.getDimension(R.styleable.WatchBoard_wb_text_size, SptoPx(16));
mHourPointWidth = array.getDimension(R.styleable.WatchBoard_wb_hour_pointer_width, DptoPx(5));
mMinutePointWidth = array.getDimension(R.styleable.WatchBoard_wb_minute_pointer_width, DptoPx(3));
mSecondPointWidth = array.getDimension(R.styleable.WatchBoard_wb_second_pointer_width, DptoPx(2));
mPointRadius = (int) array.getDimension(R.styleable.WatchBoard_wb_pointer_corner_radius, DptoPx(10));
mPointEndLength = array.getDimension(R.styleable.WatchBoard_wb_pointer_end_length, DptoPx(10));
mColorLong = array.getColor(R.styleable.WatchBoard_wb_scale_long_color, Color.argb(225, 0, 0, 0));
mColorShort = array.getColor(R.styleable.WatchBoard_wb_scale_short_color, Color.argb(125, 0, 0, 0));
mMinutePointColor = array.getColor(R.styleable.WatchBoard_wb_minute_pointer_color, Color.BLACK);
mSecondPointColor = array.getColor(R.styleable.WatchBoard_wb_second_pointer_color, Color.RED);
} catch (Exception e) {
//一旦出现错误全部使用默认值
mPadding = DptoPx(10);
mTextSize = SptoPx(16);
mHourPointWidth = DptoPx(5);
mMinutePointWidth = DptoPx(3);
mSecondPointWidth = DptoPx(2);
mPointRadius = (int) DptoPx(10);
mPointEndLength = DptoPx(10);
mColorLong = Color.argb(225, 0, 0, 0);
mColorShort = Color.argb(125, 0, 0, 0);
mMinutePointColor = Color.BLACK;
mSecondPointColor = Color.RED;
} finally {
if (array != null) {
array.recycle();
}
}
}
//Dp转px
private float DptoPx(int value) {
return SizeUtil.Dp2Px(getContext(), value);
}
//sp转px
private float SptoPx(int value) {
return SizeUtil.Sp2Px(getContext(), value);
}
SizeUtil
工具类见博客:自定义View之尺寸的转化
//画笔初始化
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
public WatchBoard(Context context, AttributeSet attrs) {
super(context, attrs);
obtainStyledAttrs(attrs); //获取自定义的属性
init(); //初始化画笔
}
view
的中间很简单,但是那样就会浪费很多的空间,于是我们应该重写onMeasure
方法,使得表盘始终只占用一个正方形的空间,但是处理的前提是用户一定会给一个确定的值,不管是宽度还是高度或者两者都是.wrap_content
的时候抛出异常,因为这样的操作对于这个组件来说是不合理的
class NoDetermineSizeException extends Exception {
public NoDetermineSizeException(String message) {
super(message);
}
}
onMeasure
方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 1000; //设定一个最小值
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED || heightMeasureSpec == MeasureSpec.AT_MOST || heightMeasureSpec == MeasureSpec.UNSPECIFIED) {
try {
throw new NoDetermineSizeException("宽度高度至少有一个确定的值,不能同时为wrap_content");
} catch (NoDetermineSizeException e) {
e.printStackTrace();
}
} else { //至少有一个为确定值,要获取其中的最小值
if (widthMode == MeasureSpec.EXACTLY) {
width = Math.min(widthSize, width);
}
if (heightMode == MeasureSpec.EXACTLY) {
width = Math.min(heightSize, width);
}
}
setMeasuredDimension(width, width);
}
match_parent
的时候也仍然只占用一个正方形)
match_parent
时占满全屏,影响其他组件的显示.)
onSizeChange
里面获取
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRadius = (Math.min(w, h) - mPadding) / 2;
mPointEndLength = mRadius / 6; //尾部指针默认为半径的六分之一
}
canvas
的坐标原点移动到中心位置
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.translate(getWidth() / 2, getHeight() / 2);
...
canvas.restore();
}
//绘制外圆背景
public void paintCircle(Canvas canvas) {
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(0, 0, mRadius, mPaint);
}
//现在onDraw方法如下
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.translate(getWidth() / 2, getHeight() / 2);
//绘制外圆背景
paintCircle(canvas);
canvas.restore();
}
60
个刻度,两个刻度之间的角度是6°
,其中包含12
个整点刻度.60
次,并且每次都是在x
轴或者y
轴上绘制.mLineWidth
,选定Y
轴绘制线条,过程如下:
60
个刻度进行判断,整点和非整点刻度设置不同的长度,颜色,宽度,绘制一个之后画布旋转6°
,即可完成所有刻度的绘制,代码如下:
//绘制刻度
private void paintScale(Canvas canvas) {
mPaint.setStrokeWidth(SizeUtil.Dp2Px(getContext(), 1));
int lineWidth = 0;
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) { //整点
mPaint.setStrokeWidth(SizeUtil.Dp2Px(getContext(), 1.5f));
mPaint.setColor(mColorLong);
lineWidth = 40;
} else { //非整点
lineWidth = 30;
mPaint.setColor(mColorShort);
mPaint.setStrokeWidth(SizeUtil.Dp2Px(getContext(), 1));
}
canvas.drawLine(0, -mRadius + SizeUtil.Dp2Px(getContext(), 10), 0, -mRadius + SizeUtil.Dp2Px(getContext(), 10) + lineWidth, mPaint);
canvas.rotate(6);
}
canvas.restore();
String text = ((i / 5) == 0 ? 12 : (i / 5)) + "";
i
为0~60
,而第一个Y
轴上绘制的应该是12
点,其他的只要对5
作取余数就可得到.canvas.save()
和canvas.restore()
.这么说可能有点难理解,看图吧:
Y坐标 = -mRadius + mLineWidth(刻度长度)+文字高度+文字与刻度的偏移量
mPaint.setTextSize(mTextSize);
String text = ((i / 5) == 0 ? 12 : (i / 5)) + "";
Rect textBound = new Rect();
mPaint.getTextBounds(text, 0, text.length(), textBound);
int textHeight = textBound.bottom - textBound.top; //获得文字高度
-mRadius + DptoPx(5) + lineWidth + (textBound.bottom - textBound.top))
-6 * i
(当前旋转的角度的负值)X,Y
坐标,注意其中的Y
坐标的基线的坐标.有不懂的同学建议看着片博客,后部分有关于绘制文字的详细内容:Android仿京东首页轮播文字(又名垂直跑马灯)
canvas.save();
canvas.translate(0, -mRadius + DptoPx(5) + lineWidth + (textBound.bottom - textBound.top));
canvas.rotate(-6 * i);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText(text, -(textBound.right - textBound.left) / 2,textBound.bottom, mPaint);
canvas.restore();
//绘制刻度
private void paintScale(Canvas canvas) {
mPaint.setStrokeWidth(SizeUtil.Dp2Px(getContext(), 1));
int lineWidth = 0;
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) { //整点
mPaint.setStrokeWidth(SizeUtil.Dp2Px(getContext(), 1.5f));
mPaint.setColor(mColorLong);
lineWidth = 40;
mPaint.setTextSize(mTextSize);
String text = ((i / 5) == 0 ? 12 : (i / 5)) + "";
Rect textBound = new Rect();
mPaint.getTextBounds(text, 0, text.length(), textBound);
mPaint.setColor(Color.BLACK);
canvas.save();
canvas.translate(0, -mRadius + DptoPx(5) + lineWidth + (textBound.bottom - textBound.top));
canvas.rotate(-6 * i);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText(text, -(textBound.right - textBound.left) / 2,textBound.bottom, mPaint);
canvas.restore();
} else { //非整点
lineWidth = 30;
mPaint.setColor(mColorShort);
mPaint.setStrokeWidth(SizeUtil.Dp2Px(getContext(), 1));
}
canvas.drawLine(0, -mRadius + SizeUtil.Dp2Px(getContext(), 10), 0, -mRadius + SizeUtil.Dp2Px(getContext(), 10) + lineWidth, mPaint);
canvas.rotate(6);
}
canvas.restore();
}
canvas.drawRoundRect
方法 ,需要指定指针的RectF
属性,为了简化计算,我们仍然采用的是在Y轴上绘制然后旋转指定角度的方法.
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR_OF_DAY); //时
int minute = calendar.get(Calendar.MINUTE); //分
int second = calendar.get(Calendar.SECOND); //秒
int angleHour = (hour % 12) * 360 / 12; //时针转过的角度
int angleMinute = minute * 360 / 60; //分针转过的角度
int angleSecond = second * 360 / 60; //秒针转过的角度
RectF
的示意图:
Y
轴绘制RoundRect
,然后旋转对应的角度即可,时分秒针旋转的角度不同,所以都需要用canvas.save()
和canvas.restore()
方法包括.直接上全部指针的代码:
private void paintPointer(Canvas canvas) {
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR_OF_DAY); //时
int minute = calendar.get(Calendar.MINUTE); //分
int second = calendar.get(Calendar.SECOND); //秒
int angleHour = (hour % 12) * 360 / 12; //时针转过的角度
int angleMinute = minute * 360 / 60; //分针转过的角度
int angleSecond = second * 360 / 60; //秒针转过的角度
//绘制时针
canvas.save();
canvas.rotate(angleHour); //旋转到时针的角度
RectF rectFHour = new RectF(-mHourPointWidth / 2, -mRadius * 3 / 5, mHourPointWidth / 2, mPointEndLength);
mPaint.setColor(mHourPointColor); //设置指针颜色
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mHourPointWidth); //设置边界宽度
canvas.drawRoundRect(rectFHour, mPointRadius, mPointRadius, mPaint); //绘制时针
canvas.restore();
//绘制分针
canvas.save();
canvas.rotate(angleMinute);
RectF rectFMinute = new RectF(-mMinutePointWidth / 2, -mRadius * 3.5f / 5, mMinutePointWidth / 2, mPointEndLength);
mPaint.setColor(mMinutePointColor);
mPaint.setStrokeWidth(mMinutePointWidth);
canvas.drawRoundRect(rectFMinute, mPointRadius, mPointRadius, mPaint);
canvas.restore();
//绘制秒针
canvas.save();
canvas.rotate(angleSecond);
RectF rectFSecond = new RectF(-mSecondPointWidth / 2, -mRadius + 15, mSecondPointWidth / 2, mPointEndLength);
mPaint.setColor(mSecondPointColor);
mPaint.setStrokeWidth(mSecondPointWidth);
canvas.drawRoundRect(rectFSecond, mPointRadius, mPointRadius, mPaint);
canvas.restore();
//绘制中心小圆
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mSecondPointColor);
canvas.drawCircle(0, 0, mSecondPointWidth * 4, mPaint);
}
onDraw()
内调用各绘制方法即可.然后每隔一秒钟刷新一次.最终的onDraw
如下:
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.translate(getWidth() / 2, getHeight() / 2);
//绘制外圆背景
paintCircle(canvas);
//绘制刻度
paintScale(canvas);
//绘制指针
paintPointer(canvas);
canvas.restore();
//刷新
postInvalidateDelayed(1000);
}
热门源码