欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

android高仿小米时钟(使用Camera和Matrix实现3D效果)

程序员文章站 2024-02-18 18:34:22
继续练习自定义view。。毕竟熟才能生巧。一直觉得小米的时钟很精美,那这次就搞它~这次除了练习自定义view,还涉及到使用camera和matrix实现3d效果。 一...

继续练习自定义view。。毕竟熟才能生巧。一直觉得小米的时钟很精美,那这次就搞它~这次除了练习自定义view,还涉及到使用camera和matrix实现3d效果。

android高仿小米时钟(使用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、准备工作做的差不多了,那就开始绘制,根据方向我先确定最外层的小时时间文本的位置及其旁边的四个弧:

android高仿小米时钟(使用Camera和Matrix实现3D效果)

注意两位数字的宽度和一位数的宽度是不一样的,在计算的时候一定要注意

 

  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()就是这段文本的高。

android高仿小米时钟(使用Camera和Matrix实现3D效果)

画文本旁边的四个弧:

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();
}

android高仿小米时钟(使用Camera和Matrix实现3D效果)

6、看实现图,时针在分针之下并且比分针颜色浅,那我就先画时针,仍然是path,并且针头为圆弧状,那么就用二阶贝赛尔曲线,路径为moveto( a),lineto(b),quadto(c,d),lineto(e),close.

android高仿小米时钟(使用Camera和Matrix实现3D效果)

/**
 * 画时针,根据不断变化的时针角度旋转画布
 * 针头为圆弧状,使用二阶贝塞尔曲线
 */
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、然后是分针,按照时针的思路:

android高仿小米时钟(使用Camera和Matrix实现3D效果)

/**
 * 画分针,根据不断变化的分针角度旋转画布
 */
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的,所以干脆画两个圆盖在上面:

android高仿小米时钟(使用Camera和Matrix实现3D效果)

/**
 * 画指针的连接圆圈,盖住指针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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。