自定义view一直是Android进阶路上的一块石头,跨过去就是垫脚石,跨不过去就是绊脚石。作为一个攻城狮,怎么能被他绊倒,一定要跟它死磕到底,这段时间看到自定义View新手实战-一步步实现精美的钟表界面特别漂亮,咱们也来手撸一个。
咱们先写一个类WatchBoard继承View,并重写他的构造方法
public class WatchBoard extends View {
public WatchBoard(Context context) {
this(context,null);
}
public WatchBoard(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public WatchBoard(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr,0);
}
public WatchBoard(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
/**
* 这是最全面构造方法的写法,存在一个问题,当minSdkVersion<21的时候,这里会出现红线,这里有两个解决办法
* 1.删除最后一个构造函数,使用前三个就可以,最直接暴力。
* 2.加入 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)或者
* @TargetApi(Build.VERSION_CODES.LOLLIPOP),
* 这里注意加入这些注解只是不出现红线,一旦使用这个构造函数在API 21以下还是会出现错误。
*/
// 这里写内容
}
在res/values包下新建attrs.xml
<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>
我们一气呵成,在类里面声明需要的属性如下
private float mRadius; // 圆形半径
private float mPadding; // 边距
private float mTextSize; // 文字大小
private float mHourPointWidth; // 时针宽度
private float mMinutePointWidth; // 分针宽度
private float mSecondPointWidth; // 秒针宽度
private float mPointRadius; // 指针圆角
private float mPointEndLength; // 指针末尾长度
private int mHourPointColor; // 时针的颜色
private int mMinutePointColor; // 分针的颜色
private int mSecondPointColor; // 秒针的颜色
private int mColorLong; // 长线的颜色
private int mColorShort; // 短线的颜色
private Paint mPaint; // 画笔
private PaintFlagsDrawFilter mDrawFilter; // 为画布设置抗锯齿
在构造方法中写一个方法取出我们的属性
/**
* @param attrs
*/
private void obtainStyledAttrs(AttributeSet attrs) {
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.WatchBoard);
mPadding = typedArray.getDimension(R.styleable.WatchBoard_wb_padding, DptoPx(10));
mTextSize = typedArray.getDimension(R.styleable.WatchBoard_wb_text_size, SptoPx(16));
mHourPointWidth = typedArray.getDimension(R.styleable.WatchBoard_wb_hour_pointer_width, DptoPx(5));
mMinutePointWidth = typedArray.getDimension(R.styleable.WatchBoard_wb_minute_pointer_width, DptoPx(3));
mSecondPointWidth = typedArray.getDimension(R.styleable.WatchBoard_wb_second_pointer_width, DptoPx(2));
mPointRadius = typedArray.getDimension(R.styleable.WatchBoard_wb_pointer_corner_radius, DptoPx(10));
mPointEndLength = typedArray.getDimension(R.styleable.WatchBoard_wb_pointer_end_length, DptoPx(10));
mHourPointColor = typedArray.getColor(R.styleable.WatchBoard_wb_hour_pointer_color, Color.BLACK);
mMinutePointColor = typedArray.getColor(R.styleable.WatchBoard_wb_minute_pointer_color, Color.BLACK);
mSecondPointColor = typedArray.getColor(R.styleable.WatchBoard_wb_second_pointer_color, Color.RED);
mColorLong = typedArray.getColor(R.styleable.WatchBoard_wb_scale_long_color, Color.argb(225, 0, 0, 0));
mColorShort = typedArray.getColor(R.styleable.WatchBoard_wb_scale_short_color, Color.argb(125, 0, 0, 0));
// 一定要回收
typedArray.recycle();
}
我们还需要一个画笔,也在构造器中初始化
public WatchBoard(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 获取属性
obtainStyledAttrs(attrs);
//初始化画笔
initPaint();
// 为画布实现抗锯齿
mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
//测量手机的宽度
int widthPixels = context.getResources().getDisplayMetrics().widthPixels;
int heightPixels = context.getResources().getDisplayMetrics().heightPixels;
// 默认和屏幕的宽高最小值相等
width = Math.min(widthPixels, heightPixels);
}
initPaint方法比较简单
private void initPaint() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
我们进行onMeasure()的测量,这里作者用了一个方法setMeasuredDimension(width,width)来保证控件的宽高相等。这里我们先给width一个固定值即屏幕的短边。
//测量手机的宽度
int widthPixels = context.getResources().getDisplayMetrics().widthPixels;
int heightPixels = context.getResources().getDisplayMetrics().heightPixels;
// 默认和屏幕的宽高最小值相等
width = Math.min(widthPixels,heightPixels);
重写onMeasure()方法,设置钟表为一个正方形
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 传入相同的数width,height,确保是正方形背景
setMeasuredDimension(measureSize(widthMeasureSpec),measureSize(heightMeasureSpec));
}
// 这里不用管测量模式是什么,因为咱们有屏幕短边保底,只取其中一个小值即可。测量宽高和屏幕短边作对比,返回最小值
private int measureSize(int measureSpec) {
int size=MeasureSpec.getSize(measureSpec);
width=Math.min(width,size);
return width;
}
为了验证结果,咱们测试一番,先将xml中修改宽高属性:
<com.example.jmf.timetest.WatchBoard
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/colorPrimary"
app:wb_scale_short_color="@color/colorAccent"/>
我们在onMeasure()方法和measureSize()方法中分别加入Log日志。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 传入相同的数width,height,确保是正方形背景
setMeasuredDimension(measureSize(widthMeasureSpec),measureSize(heightMeasureSpec));
}
// 这里不用管测量模式是什么,因为咱们有屏幕短边保底,只取其中一个小值即可。测量宽高和屏幕短边作对比,返回最小值
private int measureSize(int measureSpec) {
int size=MeasureSpec.getSize(measureSpec);
width=Math.min(width,size);
return width;
}
运行结果如下:
从钟表背景色可以看出,钟表依然是正方形。我们再来看下日志文件:
日志结果如下,onMeasure进行了两次测量,最终结果为width = 900,为什么是900呢?因为我的手机density = 3。
07-13 11:48:59.987 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard measureSize() width == 1080
07-13 11:48:59.987 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard measureSize() width == 900
07-13 11:48:59.987 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard onMeasure()i+++j ==1080+++900
07-13 11:48:59.997 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard measureSize() width == 900
07-13 11:48:59.997 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard measureSize() width == 900
07-13 11:48:59.997 8385-8385/com.example.jmf.timetest E/TAG: WatchBoard onMeasure()i+++j ==900+++900
测量完成之后我们要重写一个onSizeChanged()来获取最终测量好的width,顺便给表盘半径mRadius赋值。
@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; // 设置成半径的六分之一
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 为画布设置抗锯齿
canvas.setDrawFilter(mDrawFilter);
// 绘制半径圆
drawCircle(canvas);
}
// 绘制半径圆
private void drawCircle(Canvas canvas) {
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(width/2, width/2, mRadius, mPaint);
}
相信这里没有什么难度,效果如下
private void drawScale(Canvas canvas) {
mPaint.setStrokeWidth(SizeUtils.Dp2Px(getContext(), 1));
int lineWidth;
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) {
mPaint.setStrokeWidth(SizeUtils.Dp2Px(getContext(), 1.5f));
mPaint.setColor(mColorLong);
lineWidth = 40;
} else {
lineWidth = 30;
mPaint.setColor(mColorShort);
mPaint.setStrokeWidth(SizeUtils.Dp2Px(getContext(), 1));
}
canvas.drawLine(width/2, mPadding, width/2, mPadding + lineWidth, mPaint);
canvas.rotate(6,width/2,width/2);
}
}
String text = ((i / 5) == 0 ? 12 : (i / 5)) + "";
整体代码如下
mPaint.setStrokeWidth(SizeUtils.Dp2Px(getContext(), 1));
int lineWidth;
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) {
mPaint.setStrokeWidth(SizeUtils.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.drawText(text, width / 2 - textBound.width() / 2, textBound.height() + DptoPx(5) + lineWidth + mPadding, mPaint);
} else {
lineWidth = 30;
mPaint.setColor(mColorShort);
mPaint.setStrokeWidth(SizeUtils.Dp2Px(getContext(), 1));
}
canvas.drawLine(width / 2, mPadding, width / 2, mPadding + lineWidth, mPaint);
canvas.rotate(6, width / 2, width / 2);
}
运行效果如下,这里字体好像没有完全正过来,先不管了,接着实现指针
private void drawPointer(Canvas canvas) {
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR);// 时
int minute = calendar.get(Calendar.MINUTE);// 分
int second = calendar.get(Calendar.SECOND);// 秒
// 转过的角度
float angleHour = (hour + (float) minute / 60) * 360 / 12;
float angleMinute = (minute + (float) second / 60) * 360 / 60;
int angleSecond = second * 360 / 60;
// 绘制时针
canvas.save();
canvas.rotate(angleHour,width/2,width/2); // 旋转到时针的角度
RectF rectHour = new RectF(width/2 -mHourPointWidth / 2, width/2 -mRadius * 3 / 5, width/2 +mHourPointWidth / 2, width/2 + mPointEndLength);
mPaint.setColor(mHourPointColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mHourPointWidth);
canvas.drawRoundRect(rectHour, mPointRadius, mPointRadius, mPaint);
canvas.restore();
// 绘制分针
canvas.save();
canvas.rotate(angleMinute,width/2,width/2); // 旋转到分针的角度
RectF rectMinute = new RectF(width/2-mMinutePointWidth / 2, width/2-mRadius * 3.5f / 5, width/2+mMinutePointWidth / 2, width/2+mPointEndLength);
mPaint.setColor(mMinutePointColor);
mPaint.setStrokeWidth(mMinutePointWidth);
canvas.drawRoundRect(rectMinute, mPointRadius, mPointRadius, mPaint);
canvas.restore();
// 绘制分针
canvas.save();
canvas.rotate(angleSecond,width/2,width/2); // 旋转到分针的角度
RectF rectSecond = new RectF(width/2-mSecondPointWidth / 2, width/2-mRadius + DptoPx(10), width/2+mSecondPointWidth / 2, width/2+mPointEndLength);
mPaint.setStrokeWidth(mSecondPointWidth);
mPaint.setColor(mSecondPointColor);
canvas.drawRoundRect(rectSecond, mPointRadius, mPointRadius, mPaint);
canvas.restore();
// 绘制原点
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(width/2, width/2, mSecondPointWidth * 4, mPaint);
}
结果已经有了,现在就是要让他动起来就OK了,这里我们可以使用BroadcastReceiver来监听时间的改变,也可以直接用postInvalidateDelayed(1000)一秒后重绘一句话解决。现在onDerw()中代码如下
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.setDrawFilter(mDrawFilter);
// 绘制半径圆
drawCircle(canvas);
// 绘制刻度尺
drawScale(canvas);
// 绘制指针
drawPointer(canvas);
// 每一秒刷新一次
postInvalidateDelayed(1000);
}
这个自定义view已经搞定了,Demo已经传到Github,双手奉上
Github地址: https://github.com/Jmengfei/CustomView
源码都在里面,还会有后续的自定义view添加进去。如果对你们有帮助,不妨留个star和fork,如果有任何问题,欢迎留言指正。