书接上回:Android自定义View(一)关于super、this和构造方法
这篇自定义个表盘的CustomWatchView给大家瞅瞅,主要是会说到Canvas和Paint这两个东西。
先上个图看看效果,大概写了不到150行代码:这里大概分成两步:1是获取属性值;2是按照属性值绘图;
如果不需要在xml文件配置属性,那么在自定义类里面公开几个属性的setter就好了;Android嘛,我们当然是倾向xml配置了。
在res/values文件夹下新建attrs.xml,我们这里只是用到了主题颜色和表的半径两个属性:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CustomWatchView"> <attr name="bodyColor" format="color" /> <attr name="radius" format="float" /> </declare-styleable> </resources>
name在自定义类会当做标识来获取整个styleable,<attr/>标签定义了xml使用的名称name,和对应的值类型format。
现在有了属性定义了,接着往下走。
新建一个类命名为CustomWatchView,添加成员变量和构造方法:
public class CustomWatchView extends View{ //画笔工具 private Paint mPaint; //图像颜色 private int mColor; //表盘半径 private float radius; private float mHour = 0; private float mMinutes = 0; private float mSecond = 0; public CustomWatchView(Context context) { this(context,null); } public CustomWatchView(Context context, AttributeSet attrs) { this(context, attrs,0); } public CustomWatchView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mPaint = new Paint();//实例化画笔 //获取自定义属性列表 TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomWatchView,defStyleAttr,0); mColor = typedArray.getColor(R.styleable.CustomWatchView_bodyColor, Color.BLACK);//获取xml配置颜色,默认是黑色 radius = typedArray.getFloat(R.styleable.CustomWatchView_radius, 150);//获取xml配置的半径,默认为150 mPaint.setColor(mColor);//设置画笔颜色 mPaint.setStrokeWidth(4); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } }
可能有人注意到我在前接构造器中用的都是this,跟有些人的博客写的不一样,看过上一篇博客就知道,这么写其实是一样的,没啥问题,最终还是要调用到View(Context context)的。如果不明白,可以看一下上一篇,地址在本文顶部。
上面代码我们已经获取到了需要的变量,接下来就是绘图了:
1. 画外圈的大圆;
2. 加点自己的痕迹和文本时间(可选)。
3. 画刻度;
4. 画时针;
5. 画分针;
6. 画秒;
接下来只贴onDraw(Canvascanvas)的代码,这里把1、2一起放上来:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //画表盘 mPaint.setAntiAlias(true); //设置平滑 mPaint.setStyle(Paint.Style.STROKE); canvas.translate(getWidth()/2,getHeight()/2);//其实绘制点移动到控件的中心 canvas.drawCircle(0,0,radius,mPaint);//画圆做表盘最外围 canvas.save();//保存一下图层 canvas.translate(-70, -100);//左移70,上移100 Paint citePaint = new Paint(mPaint); citePaint.setColor(Color.GRAY); citePaint.setTextSize(28);//设置文字大小 citePaint.setStrokeWidth(2); canvas.drawText("shazeys", 20, 10, citePaint); canvas.translate(0,radius); citePaint.setColor(Color.RED); canvas.drawText(getTimeStr(),15, 10, citePaint); canvas.restore();//回复刚刚保存的位置 }
关于getTimeStr()的代码,一会儿再贴;这里出现了一对表面看起来和绘画关系不大的方法save()和restore(),关于它俩,后半部分再说,这里先露个脸,混脸熟。
到这里的效果是这样的:继续往下画刻度,代码在上段代码的下面,onDraw的内部:
//画刻度 Paint tmpPaint = new Paint(mPaint); tmpPaint.setStrokeWidth(2); float y = radius; int count = 60;//刻度总数 for (int i=0; i<count; i++){ if (i%5==0){ canvas.drawLine(0f,y,0,y-12f,mPaint);//每五个画一个大的刻度 }else{ canvas.drawLine(0f,y-7,0f,y,tmpPaint);//普通刻度 } canvas.rotate(360/count,0f,0f);//旋转画布 }
效果:
有前面的铺垫剩下的一锅上了:
//画中心的小圆和稍大的灰色小圆盘 tmpPaint.setColor(Color.GRAY); tmpPaint.setStrokeWidth(4); canvas.drawCircle(0,0,7,tmpPaint);//半径7的圆 tmpPaint.setStyle(Paint.Style.FILL); tmpPaint.setColor(mColor); canvas.drawCircle(0,0,4,tmpPaint);//半径4的圆 //画时针 canvas.rotate((float) (mHour*30 + mMinutes*0.5));//根据时间计算时针的角度,旋转画布 canvas.drawLine(0,10,0,-(radius-100),mPaint);//画时针 canvas.rotate((float) -(mHour*30 + mMinutes*0.5));//将画布转回去 //画分针 Paint miPaint = new Paint(mPaint);//新建画笔 miPaint.setColor(Color.DKGRAY);//设置颜色 miPaint.setStyle(Paint.Style.FILL); miPaint.setStrokeWidth(3); canvas.rotate(mMinutes*6);//计算分针的角度并旋转画布 canvas.drawLine(0,15,0,-(radius-50),miPaint);//画分针 canvas.rotate(-mMinutes*6);//将画布转回去 //画秒针的点 miPaint.setColor(Color.RED);//改变画笔颜色 canvas.rotate(mSecond*6);//秒针旋转角度 canvas.drawCircle(0,-y,5,miPaint); //canvas.rotate(-mSecond*6);//转回去,最后一步了,可以不恢复
基本的内容是画完了,那么作为手表,必须动起来吧?
写个定时器,启动定时器,这里使用handler来实现,下面贴一下定时器的代码:
//用Handler实现计时器 final Handler mUpdateTimeHandler = new Handler(){ @Override public void handleMessage(Message msg) { switch (msg.what){ case MSG_UPDATE_TIME: invalidate(); long time = System.currentTimeMillis(); mHour = (time/1000/60/60 + 8) % 12;//中国时区+8 mMinutes = time/1000/60 % 60; mSecond = time/1000 % 60; this.sendEmptyMessageDelayed(MSG_UPDATE_TIME,1000); Log.i("TIME",mHour+":"+mMinutes+":"+mSecond); break; } } };
对了,上面还欠个格式化时间值的方法:
//获取时间字符串 private String getTimeStr(){ return timeFormat(mHour)+":"+timeFormat(mMinutes)+":"+timeFormat(mSecond); } //格式化时间值 private String timeFormat(float value){ return value>9 ? (int)value+"" : "0"+(int)value; }公开启动和停止的两个方法:
public void start(){ mUpdateTimeHandler.sendEmptyMessageAtTime(MSG_UPDATE_TIME,0); } public void stop(){ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); }
现在这个自定义控件就整体搞完了,可以在activity中看效果了,建议吧开关写在生命周期里,节省资源。
但是现在还不能结束这篇博客,还有个坑没填:save()和restore()。
它俩是成对出现的,总是先save后restore,而且是一对一的,这个感觉就像写数据库开启事务和关闭事务一样成双成对。
save会记下当前的canvas的状态,然后在restore的时候,保留这之间的操作,然后回到save时的状态。这个再比较复杂的绘画比较常用。我们前面画时分秒的时候,都是先做了对应的旋转,然后再转回去,我们也可以改改用save和restore来处理,下面贴出用save和restore完整代码的类:
public class CustomWatchView extends View{ static final int MSG_UPDATE_TIME = 0; //画笔工具 private Paint mPaint; //图像颜色 private int mColor; //表盘半径 private float radius; private float mHour = 0; private float mMinutes = 0; private float mSecond = 0; public CustomWatchView(Context context) { this(context,null); } public CustomWatchView(Context context, AttributeSet attrs) { this(context, attrs,0); } public CustomWatchView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mPaint = new Paint();//实例化画笔 //获取自定义属性列表 TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomWatchView,defStyleAttr,0); //获取xml配置颜色,默认是黑色 mColor = typedArray.getColor(R.styleable.CustomWatchView_bodyColor, Color.BLACK); //获取xml配置的半径,默认为150 radius = typedArray.getFloat(R.styleable.CustomWatchView_radius, 150); mPaint.setColor(mColor); mPaint.setStrokeWidth(4); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //画表盘 mPaint.setAntiAlias(true); //设置平滑 mPaint.setStyle(Paint.Style.STROKE); canvas.translate(getWidth()/2,getHeight()/2);//其实绘制点移动到控件的中心 canvas.drawCircle(0,0,radius,mPaint);//画圆做表盘最外围 canvas.save();//保存一下图层 canvas.translate(-70, -100);//左移70,上移100 Paint citePaint = new Paint(mPaint); citePaint.setColor(Color.GRAY); citePaint.setTextSize(28); citePaint.setStrokeWidth(2); canvas.drawText("shazeys", 20, 10, citePaint); canvas.translate(0,radius); citePaint.setColor(Color.RED); canvas.drawText(getTimeStr(),15, 10, citePaint); canvas.restore();//回复刚刚保存的位置 //画刻度 Paint tmpPaint = new Paint(mPaint); tmpPaint.setStrokeWidth(2); float y = radius; int count = 60;//刻度总数 for (int i=0; i<count; i++){ if (i%5==0){ canvas.drawLine(0f,y,0,y-12f,mPaint);//每五个画一个大的刻度 }else{ canvas.drawLine(0f,y-7,0f,y,tmpPaint);//普通刻度 } canvas.rotate(360/count,0f,0f);//旋转画布 } //画中心的小圆和稍大的灰色小圆盘 tmpPaint.setColor(Color.GRAY); tmpPaint.setStrokeWidth(4); canvas.drawCircle(0,0,7,tmpPaint); tmpPaint.setStyle(Paint.Style.FILL); tmpPaint.setColor(mColor); canvas.drawCircle(0,0,4,tmpPaint); //画时针 canvas.save(); canvas.rotate((float) (mHour*30 + mMinutes*0.5));//根据时间计算时针的角度,旋转画布 canvas.drawLine(0,10,0,-(radius-100),mPaint);//画时针 // canvas.rotate((float) -(mHour*30 + mMinutes*0.5));//将画布转回去 canvas.restore(); //画分针 canvas.save(); Paint miPaint = new Paint(mPaint);//新建画笔 miPaint.setColor(Color.DKGRAY);//设置颜色 miPaint.setStyle(Paint.Style.FILL); miPaint.setStrokeWidth(3); canvas.rotate(mMinutes*6);//计算分针的角度并旋转画布 canvas.drawLine(0,15,0,-(radius-50),miPaint);//画分针 // canvas.rotate(-mMinutes*6);//将画布转回去 canvas.restore(); //画秒针的点 canvas.save(); miPaint.setColor(Color.RED);//改变画笔颜色 canvas.rotate(mSecond*6);//秒针旋转角度 canvas.drawCircle(0,-y,5,miPaint); //canvas.rotate(-mSecond*6);//转回去,最后一步了,可以不恢复 canvas.restore(); } //用Handler实现计时器 final Handler mUpdateTimeHandler = new Handler(){ @Override public void handleMessage(Message msg) { switch (msg.what){ case MSG_UPDATE_TIME: invalidate(); long time = System.currentTimeMillis(); mHour = (time/1000/60/60 + 8) % 12; mMinutes = time/1000/60 % 60; mSecond = time/1000 % 60; this.sendEmptyMessageDelayed(MSG_UPDATE_TIME,1000); Log.i("TIME",mHour+":"+mMinutes+":"+mSecond); break; } } }; //获取时间字符串 private String getTimeStr(){ return timeFormat(mHour)+":"+timeFormat(mMinutes)+":"+timeFormat(mSecond); } //格式化时间值 private String timeFormat(float value){ return value>9 ? (int)value+"" : "0"+(int)value; } public void start(){ mUpdateTimeHandler.sendEmptyMessageAtTime(MSG_UPDATE_TIME,0); } public void stop(){ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME); } }