当前位置: 移动技术网 > 移动技术>移动开发>Android > android高仿小米时钟(使用Camera和Matrix实现3D效果)

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

2019年07月24日  | 移动技术网移动技术  | 我要评论

继续练习自定义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下载地址:

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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网