今天继续聊自定义View,当然今天的这个比较麻烦一些
前面的自定义文字View,图片View都属于比较简单的自定义View,今天玩点有难度的,当然目的也是为了更加熟悉自定义View的各个步骤与坐标的计算、画笔的各种属性等。话不多聊 ,我们今天实现下如下的效果:
首先进行简单的分析:
钟表盘构成属性如下:
1、外部圆形边框(宽度、颜色)
2、内部一周的小黑点(宽度、颜色)
3、内部的1-12数字(字号、颜色)
4、时针分针和秒针(规格、颜色)
简单分析之后就能确定我们需要哪些的属性值,当然此处全部自定义属性,有些事没有必要的。
分析之后进行属性的自定义:
属性的自定义如下:
<declare-styleable name="DemoClockView01">
<attr name="borderwidth" format="dimension"/>
<attr name="bordercolor" format="color"/>
<attr name="pointcolor" format="color"/>
<attr name="hourcolor" format="color"/>
<attr name="minutecolor" format="color"/>
<attr name="secondcolor" format="color"/>
<attr name="numsize" format="dimension"/>
<attr name="numcolor" format="color"/>
</declare-styleable>
其中的borderwidth和bordercolor为边框的宽度和颜色,pointcolor为一圈的小黑点的颜色,hourcolor、minutecolor和secondcolor为时针、分针和秒针的颜色,numsize和numcolor为一圈数字的字号大小和颜色。
属性自定义完成之后再Java代码中拿到相关的属性值:
代码如下:
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DemoClockView01, defStyleAttr, 0);
for (int i = 0; i < array.getIndexCount(); i++) {//用getIndexCount 减少循环次数,提高性能 用.length也不能执行所有的case情况
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.DemoClockView01_borderwidth://边框宽度
mBorderWidth = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
break;
case R.styleable.DemoClockView01_bordercolor://边框颜色
mBorderColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.DemoClockView01_numcolor://数字颜色
mNumColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.DemoClockView01_numsize://数字字号
mNumSize = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
case R.styleable.DemoClockView01_pointcolor://周围小点颜色
mPointColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.DemoClockView01_hourcolor://时针颜色
mHourColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.DemoClockView01_minutecolor://分针颜色
mMinuteColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.DemoClockView01_secondcolor://秒针颜色
mSecondColor = array.getColor(attr, Color.BLACK);
break;
}
}
array.recycle();
拿到
相关
的属性之后进行控件宽和高的测量:
代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
mWidth = widthSize;
} else {
int desire = getPaddingLeft() + getPaddingRight() + (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());
mWidth = Math.min(desire, widthSize);
}
if (heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize;
} else {
int desire = getPaddingTop() + getPaddingBottom() + (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());
mHeight = Math.min(desire, heightSize);
}
mWidth = Math.min(mWidth, mHeight);//取最小值 防止绘制内容出错 以最小的边来为基准进行相关的绘制
setMeasuredDimension(mWidth, mWidth);
}
测量
完成之后进行最后的绘制,从外到内一个一个来绘制:
1、首先是外部边框,圆形,计算圆心及半径,绘制如下:
/**
* 圆心的xy和圆环的宽度
*/
final int cx, cy, width;
cx = getPaddingLeft() + (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) / 2;
cy = getPaddingTop() + (getMeasuredHeight() - getPaddingTop() - getPaddingBottom()) / 2;
width = Math.min(getWidth() / 2, getHeight() / 2);//半径
mPaint.setAntiAlias(true);//去除边缘锯齿,优化绘制效果
mPaint.setColor(mBorderColor);
if (mBorderColor == 0){
mPaint.setColor(Color.BLACK);
}
canvas.drawCircle(cx, cy, width, mPaint);//外圆 红色
mPaint.setColor(Color.WHITE);
canvas.drawCircle(cx, cy, width - mBorderWidth, mPaint);//内圆 白色
此步骤完成之后在布局文件饮用控件即可看到外部的圆环效果,如下图:
2、周围小黑点的绘制:
mPaint.setColor(mPointColor);
if (mPointColor == 0) {
mPaint.setColor(Color.BLACK);
}
canvas.save();//保存当前的状态
for (int i = 0; i < 60; i++) {//总共60个点 所以绘制60次 //绘制一圈的小黑点
if (i % 5 == 0) {
canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()), mPaint);
} else {
canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()), mPaint);
}
canvas.rotate(6, cx, cy);//360度 绘制60次 每次旋转6度
}
canvas.restore();//将canvas转回来
此步骤绘制完成之后即可看到圆环加上小圆点的效果,效果如下图:
3、数字的绘制(此处绘制的数字可能旋转了,因为绘制的时候是旋转画布绘制的,当然也可以计算每个数字点的坐标进行相关的绘制)
mPaint.setColor(mNumColor);
if (mNumColor == 0) {
mPaint.setColor(Color.BLACK);
}
mPaint.setTextSize(mNumSize);
if (mNumSize == 0) {
mPaint.setTextSize((int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
}
mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));
String[] strs = new String[]{"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11",};//绘制数字1-12 (数字角度不对 可以进行相关的处理)
Rect rect = new Rect();
canvas.save();
for (int i = 0; i < 12; i++) {//绘制12次 每次旋转30度
mPaint.getTextBounds(strs[i], 0, strs[i].length(), rect);
canvas.drawText(strs[i], cx - rect.width() / 2,
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 18, getResources().getDisplayMetrics()) + rect.height(), mPaint);
canvas.rotate(30, cx, cy);
}
canvas.restore();
此处绘制完成即可看到圆环、黑点和数字效果如下图:
4、时针、分针、秒针和圆心的绘制:
mPaint.setColor(mHourColor);
if (mHourColor == 0) {
mPaint.setColor(Color.BLACK);
}
canvas.save();//绘制时针
canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()) + rect.width(),
cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
cy, mPaint);
canvas.restore();
mPaint.setColor(mMinuteColor);
if (mMinuteColor == 0) {
mPaint.setColor(Color.BLACK);
}
canvas.save();//保存后面的状态
canvas.rotate(60, cx, cy);
canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, getResources().getDisplayMetrics()),
cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
cy, mPaint);
canvas.restore();//撤销保存的状态
mPaint.setColor(mSecondColor);
if (mSecondColor == 0) {
mPaint.setColor(Color.BLACK);
}
canvas.save();
mPaint.setColor(Color.RED);
canvas.rotate(120, cx, cy);
canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, getResources().getDisplayMetrics()),
cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
cy, mPaint);
canvas.restore();
mPaint.setColor(Color.RED);
canvas.drawCircle(cx, cy, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, getResources().getDisplayMetrics()), mPaint);//圆心,红色
此时完成即可看到完整的钟表盘效果。
如下图:
静态时钟功能到此实现。
下面呢的任务就是让其动起来:
1)时间任务,每隔1s绘制界面。
在构造方法中启动时间任务,如下:
Timer timer = new Timer("绘制线程");
timer.schedule(new TimerTask() {
@Override
public void run() {
}
}, 0, 1000);
2)计算每个时刻的对应的时针、分针、及秒针的角度。
在构造方法初始化时间如下(这里的时间类使用Calendar类):
mCalendar = Calendar.getInstance();
在onDraw方法中进行时间及角度的计算:
//关于当前时间的计算,默认为当前时间 当然是可以设置的
int hour = mCalendar.get(Calendar.HOUR);//HOUR 进制为12小时 HOUR_OF_DAY 为24小时
int minute = mCalendar.get(Calendar.MINUTE);//分钟
int second = mCalendar.get(Calendar.SECOND) + 1;//秒数
if (second == 60) {
minute += 1;
second = 0;
}
if (minute == 60){
hour += 1;
minute = 0;
}
if (hour == 12){
hour = 0;
}
mCalendar.set(Calendar.SECOND, second);
mCalendar.set(Calendar.MINUTE, minute);
mCalendar.set(Calendar.HOUR, hour);
float hourDegree = 360 * hour / 12 + 360 / 12 * minute / 60;//时针转动的角度 小时对应角度 加上 分钟对应角度 秒针忽略
float minuteDegree = 360 * minute / 60 + 360 / 60 * second / 60;//分针转动的角度 分针对应角度 加上 秒数对应角度
float secondDegree = 360 * second / 60;// 秒数对应角度
3)子线程、UI线程的切换绘制。
Handler进行子线程到子线程的转换。
最终的实现效果如下,也可以自己进行时间的设置:
到此效果实现。