android高仿小米时钟(使用Camera和Matrix实现3D效果)
继续练习自定义view。。毕竟熟才能生巧。一直觉得小米的时钟很精美,那这次就搞它~这次除了练习自定义view,还涉及到使用camera和matrix实现3d效果。
一个这样的效果,在绘制的时候最好选择一个方向一步一步的绘制,这里我选择由外到内、由深到浅的方向来绘制,代码步骤如下:
1、首先老一套~新建attrs.xml文件,编写自定义属性如时钟背景色、亮色(用于分针、秒针、渐变终止色)、暗色(圆弧、刻度线、时针、渐变起始色),新建miclockview继承view,重写构造方法,获取自定义属性值,初始化paint、path以及画圆、弧需要的rectf等东东,重写onmeasure计算宽高,这里不再啰嗦~刚开始学自定义view的同学建议从我的前几篇博客看起
2、由于onsizechanged方法在构造方法、onmeasure之后,又在ondraw之前,此时已经完成全局变量初始化,也得到了控件的宽高,所以可以在这个方法中确定一些与宽高有关的数值,比如这个view的半径啊、padding值等,方便绘制的时候计算大小和位置:
@override protected void onsizechanged(int w, int h, int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); //宽和高分别去掉padding值,取min的一半即表盘的半径 mradius = math.min(w - getpaddingleft() - getpaddingright(), h - getpaddingtop() - getpaddingbottom()) / 2; //加一个默认的padding值,为了防止用camera旋转时钟时造成四周超出view大小 mdefaultpadding = 0.12f * mradius;//根据比例确定默认padding大小 //为了适配控件大小match_parent、wrap_content、精确数值以及padding属性 mpaddingleft = mdefaultpadding + w / 2 - mradius + getpaddingleft(); mpaddingtop = mdefaultpadding + h / 2 - mradius + getpaddingtop(); mpaddingright = mpaddingleft; mpaddingbottom = mpaddingtop; mscalelength = 0.12f * mradius;//根据比例确定刻度线长度 mscalearcpaint.setstrokewidth(mscalelength);//刻度盘的弧宽 mscalelinepaint.setstrokewidth(0.012f * mradius);//刻度线的宽度 //梯度扫描渐变,以(w/2,h/2)为中心点,两种起止颜色梯度渐变 //float数组表示,[0,0.75)为起始颜色所占比例,[0.75,1}为起止颜色渐变所占比例 msweepgradient = new sweepgradient(w / 2, h / 2, new int[]{mdarkcolor, mlightcolor}, new float[]{0.75f, 1}); }
3、准备工作做的差不多了,那就开始绘制,根据方向我先确定最外层的小时时间文本的位置及其旁边的四个弧:
注意两位数字的宽度和一位数的宽度是不一样的,在计算的时候一定要注意
string timetext = "12"; mtextpaint.gettextbounds(timetext, 0, timetext.length(), mtextrect); int textlargewidth = mtextrect.width();//两位数字的宽 mcanvas.drawtext("12", getwidth() / 2 - textlargewidth / 2, mpaddingtop + mtextrect.height(), mtextpaint); timetext = "3"; mtextpaint.gettextbounds(timetext, 0, timetext.length(), mtextrect); int textsmallwidth = mtextrect.width();//一位数字的宽 mcanvas.drawtext("3", getwidth() - mpaddingright - mtextrect.height() / 2 - textsmallwidth / 2, getheight() / 2 + mtextrect.height() / 2, mtextpaint); mcanvas.drawtext("6", getwidth() / 2 - textsmallwidth / 2, getheight() - mpaddingbottom, mtextpaint); mcanvas.drawtext("9", mpaddingleft + mtextrect.height() / 2 - textsmallwidth / 2, getheight() / 2 + mtextrect.height() / 2, mtextpaint);
我计算文本的宽高一般采用的方法是,new一个rect,然后再绘制时调用
mtextpaint.gettextbounds(timetext, 0, timetext.length(), mtextrect);
将这个文本的范围赋值给这个mtextrect,此时mtextrect.width()就是这段文本的宽,mtextrect.height()就是这段文本的高。
画文本旁边的四个弧:
mcirclerectf.set(mpaddingleft + mtextrect.height() / 2 + mcirclestrokewidth / 2, mpaddingtop + mtextrect.height() / 2 + mcirclestrokewidth / 2, getwidth() - mpaddingright - mtextrect.height() / 2 + mcirclestrokewidth / 2, getheight() - mpaddingbottom - mtextrect.height() / 2 + mcirclestrokewidth / 2); for (int i = 0; i < 4; i++) { mcanvas.drawarc(mcirclerectf, 5 + 90 * i, 80, false, mcirclepaint); }
计算圆弧外接矩形的范围别忘了加上圆弧线宽的一半
4、再往里是刻度盘,画这个刻度盘的思路是现在底层画一个mscalelength宽度的圆,并设置sweepgradient渐变,上面再画一圈背景色的刻度线。获得sweepgradient的matrix对象,通过不断旋转mgradientmatrix的角度实现刻度盘的旋转效果:
/** * 画一圈梯度渲染的亮暗色渐变圆弧,重绘时不断旋转,上面盖一圈背景色的刻度线 */ private void drawscaleline() { mscalearcrectf.set(mpaddingleft + 1.5f * mscalelength + mtextrect.height() / 2, mpaddingtop + 1.5f * mscalelength + mtextrect.height() / 2, getwidth() - mpaddingright - mtextrect.height() / 2 - 1.5f * mscalelength, getheight() - mpaddingbottom - mtextrect.height() / 2 - 1.5f * mscalelength); //matrix默认会在三点钟方向开始颜色的渐变,为了吻合 //钟表十二点钟顺时针旋转的方向,把秒针旋转的角度减去90度 mgradientmatrix.setrotate(mseconddegree - 90, getwidth() / 2, getheight() / 2); msweepgradient.setlocalmatrix(mgradientmatrix); mscalearcpaint.setshader(msweepgradient); mcanvas.drawarc(mscalearcrectf, 0, 360, false, mscalearcpaint); //画背景色刻度线 mcanvas.save(); for (int i = 0; i < 200; i++) { mcanvas.drawline(getwidth() / 2, mpaddingtop + mscalelength + mtextrect.height() / 2, getwidth() / 2, mpaddingtop + 2 * mscalelength + mtextrect.height() / 2, mscalelinepaint); mcanvas.rotate(1.8f, getwidth() / 2, getheight() / 2); } mcanvas.restore(); }
这里有一个全局变量mseconddegree,即秒针旋转的角度,需要根据当前时间动态获取:
/** * 获取当前 时分秒 所对应的角度 * 为了不让秒针走得像老式挂钟一样僵硬,需要精确到毫秒 */ private void gettimedegree() { calendar calendar = calendar.getinstance(); float millisecond = calendar.get(calendar.millisecond); float second = calendar.get(calendar.second) + millisecond / 1000; float minute = calendar.get(calendar.minute) + second / 60; float hour = calendar.get(calendar.hour) + minute / 60; mseconddegree = second / 60 * 360; mminutedegree = minute / 60 * 360; mhourdegree = hour / 12 * 360; }
5、然后就是画秒针,用path绘制一个指向12点钟的三角形,通过不断旋转画布实现秒针的旋转:
/** * 画秒针,根据不断变化的秒针角度旋转画布 */ private void drawsecondhand() { mcanvas.save(); mcanvas.rotate(mseconddegree, getwidth() / 2, getheight() / 2); msecondhandpath.reset(); float offset = mpaddingtop + mtextrect.height() / 2; msecondhandpath.moveto(getwidth() / 2, offset + 0.27f * mradius); msecondhandpath.lineto(getwidth() / 2 - 0.05f * mradius, offset + 0.35f * mradius); msecondhandpath.lineto(getwidth() / 2 + 0.05f * mradius, offset + 0.35f * mradius); msecondhandpath.close(); msecondhandpaint.setcolor(mlightcolor); mcanvas.drawpath(msecondhandpath, msecondhandpaint); mcanvas.restore(); }
6、看实现图,时针在分针之下并且比分针颜色浅,那我就先画时针,仍然是path,并且针头为圆弧状,那么就用二阶贝赛尔曲线,路径为moveto( a),lineto(b),quadto(c,d),lineto(e),close.
/** * 画时针,根据不断变化的时针角度旋转画布 * 针头为圆弧状,使用二阶贝塞尔曲线 */ private void drawhourhand() { mcanvas.save(); mcanvas.rotate(mhourdegree, getwidth() / 2, getheight() / 2); mhourhandpath.reset(); float offset = mpaddingtop + mtextrect.height() / 2; mhourhandpath.moveto(getwidth() / 2 - 0.02f * mradius, getheight() / 2); mhourhandpath.lineto(getwidth() / 2 - 0.01f * mradius, offset + 0.5f * mradius); mhourhandpath.quadto(getwidth() / 2, offset + 0.48f * mradius, getwidth() / 2 + 0.01f * mradius, offset + 0.5f * mradius); mhourhandpath.lineto(getwidth() / 2 + 0.02f * mradius, getheight() / 2); mhourhandpath.close(); mcanvas.drawpath(mhourhandpath, mhourhandpaint); mcanvas.restore(); }
7、然后是分针,按照时针的思路:
/** * 画分针,根据不断变化的分针角度旋转画布 */ private void drawminutehand() { mcanvas.save(); mcanvas.rotate(mminutedegree, getwidth() / 2, getheight() / 2); mminutehandpath.reset(); float offset = mpaddingtop + mtextrect.height() / 2; mminutehandpath.moveto(getwidth() / 2 - 0.01f * mradius, getheight() / 2); mminutehandpath.lineto(getwidth() / 2 - 0.008f * mradius, offset + 0.38f * mradius); mminutehandpath.quadto(getwidth() / 2, offset + 0.36f * mradius, getwidth() / 2 + 0.008f * mradius, offset + 0.38f * mradius); mminutehandpath.lineto(getwidth() / 2 + 0.01f * mradius, getheight() / 2); mminutehandpath.close(); mcanvas.drawpath(mminutehandpath, mminutehandpaint); mcanvas.restore(); }
8、最后由于path是close的,所以干脆画两个圆盖在上面:
/** * 画指针的连接圆圈,盖住指针path在圆心的连接线 */ private void drawcovercircle() { mcanvas.drawcircle(getwidth() / 2, getheight() / 2, 0.05f * mradius, msecondhandpaint); msecondhandpaint.setcolor(mbackgroundcolor); mcanvas.drawcircle(getwidth() / 2, getheight() / 2, 0.025f * mradius, msecondhandpaint); }
9、终于画完了,ondraw部分就是这样
@override protected void ondraw(canvas canvas) { mcanvas = canvas; gettimedegree(); drawtimetext(); drawscaleline(); drawsecondhand(); drawhourhand(); drawminutehand(); drawcovercircle(); invalidate(); }
绘制的时候,尤其是像这样圆形view,灵活运用
canvas.save(); canvas.rotate(mdegree, mcenterx, mcentery); <!-- draw something --> canvas.restore();
这一套组合拳可以减少不少三角函数、角度弧度相关的计算。
10、辣么接下来就是如何实现触摸使钟表3d旋转
借助camera类和matrix类,在构造方法中:
matrix mcameramatrix = new matrix(); camera mcamera = new camera();
/** * 设置3d时钟效果,触摸矩阵的相关设置、照相机的旋转大小 * 应用在绘制图形之前,否则无效 * * @param rotatex 绕x轴旋转的大小 * @param rotatey 绕y轴旋转的大小 */ private void setcamerarotate(float rotatex, float rotatey) { mcameramatrix.reset(); mcamera.save(); mcamera.rotatex(mcamerarotatex);//绕x轴旋转角度 mcamera.rotatey(mcamerarotatey);//绕y轴旋转角度 mcamera.getmatrix(mcameramatrix);//相关属性设置到matrix中 mcamera.restore(); //camera在view左上角那个点,故旋转默认是以左上角为中心旋转 //故在动作之前pre将matrix向左移动getwidth()/2长度,向上移动getheight()/2长度 mcameramatrix.pretranslate(-getwidth() / 2, -getheight() / 2); //在动作之后post再回到原位 mcameramatrix.posttranslate(getwidth() / 2, getheight() / 2); mcanvas.concat(mcameramatrix);//matrix与canvas相关联 }
这段代码除了camera的旋转、平移、缩放之类的操作之外,剩下的代码一般是固定的
全局变量mcamerarotatex和mcamerarotatey应该与此时手指触摸坐标相关联动态获取:
@override public boolean ontouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: getcamerarotate(event); break; case motionevent.action_move: //根据手指坐标计算camera应该旋转的大小 getcamerarotate(event); break; } return true; }
camera的坐标系和view的坐标系是不一样的
view坐标系是二维的,原点在屏幕左上角,右为x轴正方向,下为y轴正方向;而camera坐标系是三维的,原点在屏幕左上角,右为x轴正方向,上为y轴正方向,屏幕向里为z轴正方向
/** * 获取camera旋转的大小 * 注意view坐标与camera坐标方向的转换 */ private void getcamerarotate(motionevent event) { float rotatex = -(event.gety() - getheight() / 2); float rotatey = (event.getx() - getwidth() / 2); //求出此时旋转的大小与半径之比 float percentx = rotatex / mradius; float percenty = rotatey / mradius; if (percentx > 1) { percentx = 1; } else if (percentx < -1) { percentx = -1; } if (percenty > 1) { percenty = 1; } else if (percenty < -1) { percenty = -1; } //最终旋转的大小按比例匀称改变 mcamerarotatex = percentx * mmaxcamerarotate; mcamerarotatey = percenty * mmaxcamerarotate; }
11、最后在ontouchevent中松开手指时加一个复原并晃动的动画
case motionevent.action_up: //松开手指,时钟复原并伴随晃动动画 valueanimator animx = getshakeanim(mcamerarotatex, 0); animx.addupdatelistener(new valueanimator.animatorupdatelistener() { @override public void onanimationupdate(valueanimator valueanimator) { mcamerarotatex = (float) valueanimator.getanimatedvalue(); } }); valueanimator animy = getshakeanim(mcamerarotatey, 0); animy.addupdatelistener(new valueanimator.animatorupdatelistener() { @override public void onanimationupdate(valueanimator valueanimator) { mcamerarotatey = (float) valueanimator.getanimatedvalue(); } }); break;
/** * 使用overshootinterpolator完成时钟晃动动画 */ private valueanimator getshakeanim(float start, float end) { valueanimator anim = valueanimator.offloat(start, end); anim.setinterpolator(new overshootinterpolator(10)); anim.setduration(500); anim.start(); return anim; }
终于写完了,这个miclockview适配也做的差不多了,时间也是同步的手机时间,一般可以拿来就用了~
demo下载地址:http://xiazai.jb51.net/201701/yuanma/miclockview_jb51.rar
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。