转自http://blog.csdn.net/qq_31715429/article/details/54668668

附上github地址:
https://github.com/MonkeyMushroom/MiClockView
欢迎star~

一个这样的效果,在绘制的时候最好选择一个方向一步一步的绘制,这里我选择由外到内、由深到浅的方向来绘制,代码步骤如下:

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旋转的大小*/
private void getCameraRotate(MotionEvent event) {if (mShakeAnim != null && mShakeAnim.isRunning()) {mShakeAnim.cancel();}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;
}

解释一下camera旋转角度为啥介么算:

    float rotateX = -(event.getY() - getHeight() / 2);float rotateY = (event.getX() - getWidth() / 2);

是这样的,当camer.rotateX(x)的x为正时,图像绕X轴上半部分向里下半部分向外旋转,也就是手指触摸点就要往上移。这个x就会与event.getY()的值有关,x越大,绕X轴旋转角度越大,以圆心为原点,往上event.getY() - getHeight() / 2的值为负,故 float rotateX = -(event.getY() - getHeight() / 2);

而对于camer.rotateY(y)的y为正时,图像绕Y轴右半部分向里左半部分向外旋转,也就是手指触摸点就要往右移。这个y就会与event.getX()的值有关,y越大,绕Y轴旋转角度越大,以圆心为原点,往上event.getX() - getWidth() / 2的值为正,故 float rotateY = event.getX() - getWidth() / 2。

其他情况大家可以试一下,百度一下camera的坐标以及它的旋转是怎么转的~


11、最后在onTouchEvent中松开手指时加一个复原并晃动的动画

case MotionEvent.ACTION_UP://松开手指,时钟复原并伴随晃动动画startShakeAnim();break;
/*** 使用OvershootInterpolator完成时钟晃动动画*/
private void startShakeAnim() {final String cameraRotateXName = "cameraRotateX";final String cameraRotateYName = "cameraRotateY";PropertyValuesHolder cameraRotateXHolder =PropertyValuesHolder.ofFloat(cameraRotateXName, mCameraRotateX, 0);PropertyValuesHolder cameraRotateYHolder =PropertyValuesHolder.ofFloat(cameraRotateYName, mCameraRotateY, 0);mShakeAnim = ValueAnimator.ofPropertyValuesHolder(cameraRotateXHolder, cameraRotateYHolder);mShakeAnim.setInterpolator(new OvershootInterpolator(10));mShakeAnim.setDuration(500);mShakeAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mCameraRotateX = (float) animation.getAnimatedValue(cameraRotateXName);mCameraRotateY = (float) animation.getAnimatedValue(cameraRotateYName);}});mShakeAnim.start();
}

终于写完了,这个MiClockView适配也做的差不多了,时间也是同步的手机时间,一般可以拿来就用了~

后来

额,经过我的细心观察。。发现拨动时钟时,时针、分针、秒针和刻度盘会有一个较小的偏移量,形成有层次的、近大远小的立体偏移效果。。本来打算用 matrix 和 camera 的 mCamera.translate(x, y, z) 方法改变 z 的值,随着z值增大,原先计算好的大小只会变小,并不会层叠偏移。。所以就随着手指移动动态计算位移距离,然后在 onDraw()的绘制不同零件的方法中不断 mCanvas.translate(x, y) 达到类似立体偏移的效果。

源码奉上:https://github.com/MonkeyMushroom/MiClockView
欢迎star~

高仿小米时钟 - 使用Camera和Matrix实现3D效果相关推荐

  1. 60.自定义View练习(五)高仿小米时钟 - 使用Camera和Matrix实现3D效果

    *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 本文出自:猴菇先生的博客 http://blog.csdn.net/qq_31715429/article/details/546 ...

  2. android高仿小米时钟,Android小米时钟 Android仿小米时钟效果

    想了解Android仿小米时钟效果的相关内容吗,jane_dxj在本文为您仔细讲解Android小米时钟的相关知识和一些Code实例,欢迎阅读和指正,我们先划重点:Android,时钟,下面大家一起来 ...

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

    继续练习自定义View..毕竟熟才能生巧.一直觉得小米的时钟很精美,那这次就搞它~这次除了练习自定义View,还涉及到使用Camera和Matrix实现3D效果. 一个这样的效果,在绘制的时候最好选择 ...

  4. android高仿小米时钟,Android高仿小米时钟

    今日科技快讯 昨日,小米科技正式发布首款自主芯片松果澎湃S1和搭载澎湃S1芯片的小米5C手机.从立项做到量产结束,小米芯片用了28个月.S1的CPU为八核64位处理器,主频达2.2GHz,处理高计算量 ...

  5. 使用Camera和Matrix实现3D效果

    本文出自:猴菇先生的博客  http://blog.csdn.net/qq_31715429/article/details/54668668 继续练习自定义View..毕竟熟才能生巧.一直觉得小米的 ...

  6. Android仿小米时钟

    最近在学自定义View,看到小米时钟这个效果,很想去了解一下是界面是如何绘制以及秒针,分针,时针是如何转动的,还有那个弧形颜色渐变效果.这是我参照Github上的效果弄的,Github地址:高仿小米时 ...

  7. android小记之FlashLight --- 高仿小米手电筒

    玩小米有段时间了,发现还蛮好玩的.小米的手电筒UI蛮漂亮的,哥手贱,也仿了一个,纯属娱乐. 有图有码有真相: 代码简单到,我都不想解释了,直接上码吧. /** * * @author scott * ...

  8. 小马哥-----高仿小米 note刷机 h78机型型号 机型新旧版机型对比图与开机识别对比图

    高仿小米note H78机型型号 日期版本有分别 刷错会导致刷机平台刷对应资料代码提示4032 具体主板型号一样,背面标示贴稍有区别 看中壳背面对比图   警惕

  9. 小马哥----高仿小米4 tc01刷机拆机主板图与开机界面图 分联通版与移动版

    高仿米4 版本种类较多 m8209 m8219 T7219 T7220  T8221  T8213  H69 H71 K6  X77 TC01  TD01 x65等等机型版本 陆续有新版本出现市场 高 ...

最新文章

  1. mysql索引底层实现原理_mysql的索引底层之实现原理
  2. 究竟是该采用面向服务结构,还是单体结构
  3. 无法在终端中显示Git树
  4. 业务时间做开发,使用jeecg框架
  5. 机器学习总结(17)-XGBoost
  6. x什么意思c语言新闻app啊我et,C语言笔试题目
  7. 马云身家将超4800亿!蚂蚁集团IPO发行价出炉,总市值达2.1万亿
  8. 你需要启用steam社区界面功能以进行购买_打开了新世界的大门!Steam好评的实用工具/软件推荐...
  9. 网络TCP/IP基础(IP地址网络汇总与规划)
  10. CentOS开机简要流程
  11. 使用uniapp开发微信小程序的人脸采集功能/人脸识别功能
  12. matlab图像的错切变换,matlab 图像几何变换+答案
  13. cJson使用的简单例子
  14. drozer安装与使用
  15. 【Markdown 1】什么是Markdown?怎么用? - Markdown语法个人简述总结(图文)(常用)
  16. A5931三相无传感器 BLDC 风扇驱动器 IC
  17. UNI-APP在自定义组件中内嵌H5/Html网页,可自定义webview大小,加载不闪屏
  18. Oracle中的emp、dept、bonus及salgrade表的新建及MySQL中的三种注释形式
  19. Java Web应用开发——作业一
  20. 【毕业设计系列】035:基于matlab的线性调频信号的仿真

热门文章

  1. 115200波特率-串口基本知识
  2. 设备网络搜索软件 SADPTool V3.0.0.16
  3. JSch简介以及jsch实现文件上传下载
  4. 新闻速递|伦敦帝国理工学院大刀阔斧改革科研成果转化机制
  5. 【KDD20】多变量时间序列异常检测算法之USAD:对抗性训练AE
  6. 车辆超速监控系统c语言编程,c语言车辆违章管理系统
  7. 小木虫emuch遭封禁,新域名muchong.com尚可用
  8. android意图相机代码,Android从相机获取图像Uri
  9. pr为解说视频添加字幕
  10. matlab里面的cov函数,如何强制Matlab /八度cov函数使用可选参数