*本篇文章已授权微信公众号 guolin_blog (郭霖)独家公布

本文出自:猴菇先生的博客

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

继续练习自己定义View。。

毕竟熟才干生巧。一直认为小米的时钟非常精美。那这次就搞它~这次除了练习自己定义View,还涉及到使用Camera和Matrix实现3D效果。

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcXFfMzE3MTU0Mjk=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">

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、然后是分针,依照时针的思路:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcXFfMzE3MTU0Mjk=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcXFfMzE3MTU0Mjk=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">

/**
* 画指针的连接圆圈,盖住指针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() {
@Override
public 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~

60.自己定义View练习(五)高仿小米时钟 - 使用Camera和Matrix实现3D效果的更多相关文章

  1. 高仿京东到家APP引导页炫酷动画效果

    前言 京东到家APP的引导页做的可圈可点,插画+动效,简明生动地说明了APP最吸引用户的几个亮点(商品多,价格低,配送快...).本文主要分析拆解这些动画效果,并完成一个高仿Demo,完整的Demo代 ...

  2. android高仿微信UI点击头像显示大图片效果

    用过微信的朋友朋友都见过微信中点击对方头像显示会加载大图,先贴两张图片说明下: 这种UI效果对用户的体验不错,今天突然有了灵感,试着去实现,结果就出来了.. 下面说说我的思路: 1.点击图片时跳转到另 ...

  3. android高仿微信UI点击头像显示大图片效果, Android 使用ContentProvider扫描手机中的图片,仿微信显示本地图片效果

    http://www.cnblogs.com/Jaylong/archive/2012/09/27/androidUI.html http://blog.csdn.net/xiaanming/arti ...

  4. android高仿抖音、点餐界面、天气项目、自定义view指示、爬取美女图片等源码

    Android精选源码 一个爬取美女图片的app Android高仿抖音 android一个可以上拉下滑的Ui效果 android用shape方式实现样式源码 一款Android上的新浪微博第三方轻量 ...

  5. Android -- 真正的 高仿微信 打开网页的进度条效果

    (本博客为原创,http://www.cnblogs.com/linguanh/) 目录: 一,为什么说是真正的高仿? 二,为什么要搞缓慢效果? 三,我的实现思路 四,代码,内含注释 五,使用方法与截 ...

  6. Android ActionBar应用实战,高仿微信主界面的设计

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/26365683 经过前面两篇文章的学习,我想大家对ActionBar都已经有一个相对 ...

  7. Android高级控件(六)——自定义ListView高仿一个QQ可拖拽列表的实现

    Android高级控件(六)--自定义ListView高仿一个QQ可拖拽列表的实现 我们做一些好友列表或者商品列表的时候,居多的需求可能就是需要列表拖拽了,而我们选择了ListView,也是因为使用L ...

  8. 安卓高仿QQ头像截取升级版

    观看此篇文章前,请先阅读上篇文章:高仿QQ头像截取: 本篇之所以为升级版,是在截取头像界面添加了与qq类似的阴影层(裁剪区域以外的部分),且看效果图:   为了适应大家不同需求,这次打了两个包,及上图 ...

  9. 自己定义View步骤

     概述 Android已经为我们提供了大量的View供我们使用,可是可能有时候这些组件不能满足我们的需求,这时候就须要自己定义控件了.自己定义控件对于刚開始学习的人总是感觉是一种复杂的技术. 由于 ...

随机推荐

  1. 【重要】python之模块CGI 通用网关接口

    # -*- coding: utf-8 -*- #python 27 #xiaodeng #CGI模块 import CGI #通用网关接口,它是一段程序,运行在服务器上如:HTTP服务器,提供同客户 ...

  2. Lotusscript统计在线用户数

    使用notessession的SendConsoleCommand方法向服务器控制台发送“show inetusers”命令,该命令返回一个结果(字符串),字符串类似如下: admin   192.1 ...

  3. 一种通过MQ使缓存和数据库同步的玩法

    其他相关玩法 可以搜索 mysql 和 redis 结合使用

  4. adb shell中的am pm命令

    adb shell中的am pm命令,一些自己的见解和大多数官网的翻译. am命令 am全称activity manager,你能使用am去模拟各种系统的行为,例如去启动一个activity,强制停止 ...

  5. 2016年排名Top 100的Java类库——在分析了47,251个依赖之后得出的结论(16年文章)

    本文由HollisChuang 翻译自 The Top 100 Java Libraries in 2016 – After Analyzing 47,251 Dependencies . 原作者:H ...

  6. Easyui入门视频教程 第05集---Easyui复杂布局

    目录 ----------------------- Easyui入门视频教程 第09集---登录完善 图标自定义   Easyui入门视频教程 第08集---登录实现 ajax button的使用  ...

  7. Kubernetes的Cron Job

    Kubernetes集群使用Cron Job管理基于时间的作业,可以在指定的时间点执行一次或在指定时间点执行多次任务. 一个Cron Job就好像Linux crontab中的一行,可以按照Cron定 ...

  8. 聚集函数查询结果为空, list的size是1, resolve

    resultList.removeAll(Collections.singleton(null));

  9. JavaScript 浏览器对象模型 (BOM)

    浏览器对象模型 (BOM) 使 JavaScript 有能力与浏览器“对话”. 浏览器对象模型 (BOM) 浏览器对象模型(Browser Object Model)尚无正式标准. 由于现代浏览器已经 ...

  10. iOS 持续集成

    iOS 持续集成系列 - 开篇 前言 iOS 开发在经过这几年的野蛮生长之后,慢慢地趋于稳定.无论开发语言是 Objective-C 还是 Swift,工程类型是 Hybird 还是原生,开发思想是 ...