引言

今天玩小米mix2的时候看到了小米的时间控件效果真的很棒。有各种动画效果,3d触摸效果,然后就想着自己能不能也实现一个这样的时间控件,那就开始行动绘制一个简易版本的小米时间控件吧o((≧▽≦o)

效果图

  • 首先来看看小米的效果是这个样子的
  • 再来看看我的效果

具体实现过程

我们都知道自定控件的绘制有很多种,继承view,继承viewgroup,还有继承已有的控件,但是无非就几个步骤:

  • measure(): 测量,用来控制控件的大小,final 不建议复写
  • layout(): 布局,用来控制控件摆放的位置,继承view不需要
  • draw(): 绘制,用来控制控件的样子
  • ontouch();touch事件

其中最重要的就是draw()方法,这个3d触摸效果是采用了Camera与Matrix现实,话不多说,开始理一下自己绘制的思路,我设想的绘制思路如下:

  • 1,先画最外层的圆弧和文字
  • 2,再画里面刻度盘
  • 3,再画秒表三角形
  • 4,画时针和分针
  • 5,画中间小球
  • 6,3d触摸效果

既然思路已经明确了,二话不说,那就开始搞吧

绘制前的准备工作

首先需要初始化各种画笔,kotlin提供了一个init方法,就是拿来初始化的,不用像以前那样每个构造够一个init方法了,简单快捷

init {mPaintOutCircle.color = color_halfWhitemPaintOutCircle.strokeWidth = dp2px(1f)mPaintOutCircle.style = Paint.Style.STROKEmPaintOutText.color = color_halfWhitemPaintOutText.strokeWidth = dp2px(1f)mPaintOutText.style = Paint.Style.STROKEmPaintOutText.textSize = sp2px(10f).toFloat()mPaintOutText.textAlign = Paint.Align.CENTERmPaintProgressBg.color = color_halfWhitemPaintProgressBg.strokeWidth = dp2px(2f)mPaintProgressBg.style = Paint.Style.STROKEmPaintProgress.color = color_halfWhitemPaintProgress.strokeWidth = dp2px(2f)mPaintProgress.style = Paint.Style.STROKEmPaintTriangle.color = color_whitemPaintTriangle.style = Paint.Style.FILLmPaintHour.color = color_halfWhitemPaintHour.style = Paint.Style.FILLmPaintMinute.color = color_whitemPaintMinute.strokeWidth = dp2px(3f)mPaintMinute.style = Paint.Style.STROKEmPaintMinute.strokeCap = Paint.Cap.ROUNDmPaintBall.color = Color.parseColor("#836FFF")mPaintBall.style = Paint.Style.FILL}

接下来我们来看一下onmeasure方法,这个是测量控件的方法,一般情况下我们是不需要复写的,但是这个是个正方形的控件,所以是需要复写的,不能随便设置宽高,宽高需要保持一致;来看下代码

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {val width = View.MeasureSpec.getSize(widthMeasureSpec)val height = View.MeasureSpec.getSize(heightMeasureSpec)//设置为正方形val imageSize = if (width < height) width else heightsetMeasuredDimension(imageSize, imageSize)}

然后我们就开始绘制了,就复写ondraw方法就行了,在绘制之前我们需要把cavans画板平移到view的中心,这样有利于下面绘制时候的旋转绘制,这里说明一下,我采用的是旋转画布的绘制方式,当然你也可以采用三角函数进行计算具体的位置进行绘制,道理差不多,我只是觉得计算麻烦一点;

我们来看一下实现,首先需要在onSizeChanged()方法拿到宽高,这个方法调用的时候代表已经测量结束,所以是可以拿到宽高的;

  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)mWidth = wmHeight = hmCenterX = mWidth / 2mCenterY = mHeight / 2}

然后画布平移

   override fun onDraw(canvas: Canvas) {//平移到视图中心canvas.translate(mCenterX.toFloat(), mCenterY.toFloat())}

好了绘制前的准备工作已经做好了接下来开始进行绘制

绘制外边缘4个弧度

按照我之前的绘制思路,已经是首先绘制外边缘的4个弧度,每个弧度我设置为80°,弧度的位置分别是是
5-85,95-175,185 -265,275-355;

    private fun drawArcCircle(canvas: Canvas) {val min = Math.min(width, mHeight)val rect = RectF(-(min - paddingOut) / 2, -(min - paddingOut) / 2, (min - paddingOut) / 2, (min - paddingOut) / 2)canvas.drawArc(rect, 5f, 80f, false, mPaintOutCircle)canvas.drawArc(rect, 95f, 80f, false, mPaintOutCircle)canvas.drawArc(rect, 185f, 80f, false, mPaintOutCircle)canvas.drawArc(rect, 275f, 80f, false, mPaintOutCircle)}

看下效果

绘制外边缘4个文字

然后绘制4个文字刻度:3,6,9,12,文字绘制主要是要把握到文字的具体位置,这里还是有点麻烦滴,你要知道具体的文字画笔几个属性是什么意思,如下图


上代码
private fun drawOutText(canvas: Canvas) {val min = Math.min(width, mHeight)val textRadius = (min - paddingOut) / 2val fm = mPaintOutText.getFontMetrics()//文字的高度val mTxtHeight = Math.ceil((fm.leading - fm.ascent).toDouble()).toInt()canvas.drawText("3", textRadius, (mTxtHeight / 2).toFloat(), mPaintOutText)canvas.drawText("9", -textRadius, (mTxtHeight / 2).toFloat(), mPaintOutText)canvas.drawText("6", 0f, textRadius + mTxtHeight / 2, mPaintOutText)canvas.drawText("12", 0f, -textRadius + mTxtHeight / 2, mPaintOutText)}

效果图

绘制刻度

刻度就很简单了,每个2°绘制一个刻度,也就是有180个刻度

  private fun drawCalibrationLine(canvas: Canvas) {val min = Math.min(width, mHeight) / 2for (i in 0 until 360 step 2) {canvas.save()canvas.rotate(i.toFloat())canvas.drawLine(min.toFloat() * 3 / 4, 0f, min * 3 / 4 + dp2px(10f), 0f, mPaintProgressBg);canvas.restore()}}

如图

绘制秒

这个是最有难度的一个地方,三角形通过旋转画布实现,知道秒表走的角度就可以了

   private fun drawSecond(canvas: Canvas) {//先绘制秒针的三角形canvas.save()canvas.rotate(mSecondMillsDegress)val path = Path()path.moveTo(0f, -width * 3f / 8 + dp2px(5f))path.lineTo(dp2px(8f), -width * 3f / 8 + dp2px(20f))path.lineTo(-dp2px(8f), -width * 3f / 8 + dp2px(20f))path.close()canvas.drawPath(path, mPaintTriangle)canvas.restore()//绘制渐变刻度val min = Math.min(width, mHeight) / 2for (i in 0..90 step 2) {//第一个参数设置透明度,实现渐变效果,从255到0canvas.save()mPaintProgress.setARGB((255 - 2.7 * i).toInt(), 255, 255, 255)//这里的先减去90°,是为了旋转到开始角度,因为开始角度是y轴的负方向canvas.rotate(((mSecondDegress - 90 - i).toFloat()))canvas.drawLine(min.toFloat() * 3 / 4, 0f, min * 3 / 4 + dp2px(10f), 0f, mPaintProgress);canvas.restore()}}

如图

绘制分针和时针

分针是一阶贝塞尔就是画线,时针是画path,也是旋转角度,这两个都差不多,就是绘制path

   private fun drawMinute(canvas: Canvas) {canvas.save()canvas.rotate(mMinuteDegress.toFloat())canvas.drawLine(0f, 0f, 0f, -(width / 3).toFloat(), mPaintMinute)canvas.restore()}private fun drawHour(canvas: Canvas) {canvas.save()canvas.rotate(mHourDegress.toFloat())canvas.drawCircle(0f, 0f, innerRadius, mPaintTriangle)val path = Path()path.moveTo(-innerRadius / 2, 0f)path.lineTo(innerRadius / 2, 0f)path.lineTo(innerRadius / 6, -(width / 4).toFloat())path.lineTo(-innerRadius / 6, -(width / 4).toFloat())path.close()canvas.drawPath(path, mPaintHour)canvas.restore()}

绘制中间小球

这个就不用多说了

  private fun drawBall(canvas: Canvas) {canvas.drawCircle(0f, 0f, innerRadius / 2, mPaintBall)}

绘制就结束了,接下来是如何让时间走起来
如图

让时间走起来

我才用的方法就是直接采用延时任务的方式,每隔150毫秒去刷新一次时间数据
首先要获取到具体的当前时间数据

  private fun calculateDegree() {val mCalendar = Calendar.getInstance()mCalendar.timeInMillis = System.currentTimeMillis()val minute = mCalendar.get(Calendar.MINUTE)val secondMills = mCalendar.get(Calendar.MILLISECOND)val second = mCalendar.get(Calendar.SECOND)val hour = mCalendar.get(Calendar.HOUR)mHourDegress = hour * 30mMinuteDegress = minute * 6mSecondMillsDegress = second * 6 + secondMills * 0.006fmSecondDegress = second * 6val mills = secondMills * 0.006f //因为是没2°旋转一个刻度,所以这里要根据毫秒值来进行计算when (mills) {in 2 until 4 -> {mSecondDegress +=2}in 4 until 6 -> {mSecondDegress += 4}}}

然后开始运动

 // 指针转动的方法fun startTick() {// 一秒钟刷新一次postDelayed(mRunnable, 150)}private val mRunnable = Runnable {calculateDegree()invalidate()startTick()}

最后跟window进行视图绑定

/*** 调用时机:onAttachedToWindow是在第一次onDraw前调用的,只调用一次*/override fun onAttachedToWindow() {super.onAttachedToWindow()startTick()}/*** 调用时机:我们销毁View的时候。我们写的这个View不再显示。*/override fun onDetachedFromWindow() {super.onDetachedFromWindow()removeCallbacks(mRunnable)}

3d触效果

3d效果主要涉及到两个类

一个是android.graphics.Camera:3D开发,不是android.hardware.Camera:别倒错包了
还有一个是矩阵Matrix

一个照相机实例可以被用于计算3D变换,生成一个可以被使用的Matrix矩阵,一个实例,用在画布上。
其实Camera内部机制实际上还是opengl,只不过大大简化了使用。这样有利于开发者进行开发

Camera的坐标系是左手坐标系。当手机平整的放在桌面上,X轴是手机的水平方向,Y轴是手机的竖直方向,Z轴是垂直于手机向里的那个方向。

这个控件首先要touch事件中获取到具体的旋转角度,也就是说我们在触摸的时候计算出我们需要旋转的角度,然后设置给camera,然后camera在设置给Matrix,最后关联cavans

主要代码(部分代码参考自定义View练习(五)高仿小米时钟)

    private fun setCameraRotate() {mCameraMatrix.reset()mCamera.save()mCamera.rotateX(mCameraRotateX)//绕x轴旋转角度mCamera.rotateY(mCameraRotateY)//绕y轴旋转角度mCamera.getMatrix(mCameraMatrix)//相关属性设置到matrix中mCamera.restore()mCanvas.concat(mCameraMatrix)//matrix与canvas相关联}

touch事件的处理

    override fun onTouchEvent(event: MotionEvent): Boolean {when (event.action) {MotionEvent.ACTION_DOWN -> {mShakeAnim?.let {if (it.isRunning) {it.cancel()}}getCameraRotate(event)getCanvasTranslate(event)}MotionEvent.ACTION_MOVE -> {//根据手指坐标计算camera应该旋转的大小getCameraRotate(event)getCanvasTranslate(event)}MotionEvent.ACTION_UP ->//松开手指,时钟复原并伴随晃动动画startShakeAnim()}return true}
 private fun getCameraRotate(event: MotionEvent) {val rotateX = -(event.y - height / 2)val rotateY = event.x - width / 2//求出此时旋转的大小与半径之比val percentArr = getPercent(rotateX, rotateY)//最终旋转的大小按比例匀称改变mCameraRotateX = percentArr[0] * mMaxCameraRotatemCameraRotateY = percentArr[1] * mMaxCameraRotate}/*** 当拨动时钟时,会发现时针、分针、秒针和刻度盘会有一个较小的偏移量,形成近大远小的立体偏移效果* 一开始我打算使用 matrix 和 camera 的 mCamera.translate(x, y, z) 方法改变 z 的值*/private fun getCanvasTranslate(event: MotionEvent) {val translateX = event.x - width / 2val translateY = event.y - height / 2//求出此时位移的大小与半径之比val percentArr = getPercent(translateX, translateY)//最终位移的大小按比例匀称改变mCanvasTranslateX = percentArr[0] * mMaxCanvasTranslatemCanvasTranslateY = percentArr[1] * mMaxCanvasTranslate}/*** 获取一个操作旋转或位移大小的比例* @return 装有xy比例的float数组*/private fun getPercent(x: Float, y: Float): FloatArray {val percentArr = FloatArray(2)var percentX = x / mRadiusvar percentY = y / mRadiusif (percentX > 1) {percentX = 1f} else if (percentX < -1) {percentX = -1f}if (percentY > 1) {percentY = 1f} else if (percentY < -1) {percentY = -1f}percentArr[0] = percentXpercentArr[1] = percentYreturn percentArr}

就这样一个小米时间控件绘制完成了
当然我也传了一个demo到github,如果有需要可以去下载玩玩
地址:github地址

自定义view之kotlin绘制精简小米时间控件相关推荐

  1. Android自定义View精品(CustomCalendar-定制日历控件)

    版权声明:本文为openXu原创文章[openXu的博客],未经博主允许不得以任何形式转载 目录: 文章目录 1.分析 2.自定义属性 3.onMeasure() 4.onDraw() ①.绘制月份 ...

  2. 玩转自定义View之大学问特色蛛网评分控件

    在github上搜了一堆堆评分控件都没有理想中的样子所以在自己的开源项目上造了了轮子出来效果图如下: 先说明下理想中需求 支持任意大于等于3的评分 支持具有变色效果 支持分数以及图形分平均值描边 支持 ...

  3. 自定义view实战(11):滑动解锁九宫格控件

    前言 上一篇文章用贝塞尔曲线画了一个看起来不错的小红点功能,技术上没什么难度,主要就是数学上的计算.这篇文章也差不多,模仿了一个常用的滑动解锁的九宫格控件. 需求 用过安卓的都知道,用过苹果的也知道, ...

  4. 自定义view,仿微信、支付宝密码输入控件的源码实现

    研究支付宝密码输入控件及源码实现 目标效果图 实现思路 要想实现输入,就少不了EditText 看整体布局应该是一个横向的LinearLayout 每个格子看进来应该是多个子View 那么我们是不是有 ...

  5. 精通Android自定义View(十四)绘制水平向右加载的进度条

    1引言 1 精通Android自定义View(一)View的绘制流程简述 2 精通Android自定义View(二)View绘制三部曲 3 精通Android自定义View(三)View绘制三部曲综合 ...

  6. 精通Android自定义View(十二)绘制圆形进度条

    1 绘图基础简析 1 精通Android自定义View(一)View的绘制流程简述 2 精通Android自定义View(二)View绘制三部曲 3 精通Android自定义View(三)View绘制 ...

  7. android自定义虚线,Android自定义view的方式绘制虚线

    Android自定义view绘制虚线 最近项目中有个需求,通过自定义view的方式绘制虚线 别的不多说先看一眼效果 这个需求在我们的开发中应该是一个很常见的需求了吧,有人会说有更简单的实现方式,对,但 ...

  8. Android自定义时间控件不可选择未来时间

    本文出自:http://blog.csdn.net/dt235201314/article/details/78718066 Android自定义时间控件选择开始时间到结束时间 Android自定义时 ...

  9. WPF自定义日期时间控件

    WPF自定义日期时间控件 一.需求分析 二.功能实现 一.需求分析 在工作中遇到的项目中,大部分软件是处于全屏运行状态,这时候就需要在软件的界面上加上日期时间那些,方便用户查看当前时间. 二.功能实现 ...

最新文章

  1. 京东网络开放之路——自研交换机探索与实践
  2. 网站PC端跟移动端有哪些不同的区别所在?
  3. vue的路由与es6的import, export
  4. mongodb type
  5. fn hotkeys and osd_潍坊实习生活(3)and 绊 最后的进化
  6. react优秀项目案例_2020中国5G+工业互联网大会:鄂州2项目现场签约,2项目入选十大优秀案例...
  7. Android多渠道打包APK
  8. java支付宝开发-00-资源帖
  9. 基于C#实现的个人日程管理系统
  10. html快闪软件制作,教你如何用PPT轻松完成快闪视频制作?
  11. 考研英语大纲单词E~O与常用短语
  12. 博雅数智|第四次直播|PageRank算法
  13. AutoCAD如何创建图层?开关图层、冻结图层、锁定图层怎么运用?
  14. 阿里顶级架构师倾情推荐:国内首本大型分布式架构笔记《凤凰架构》
  15. 贴片电解电容100UF16V 6.3*4.5mm超薄封装规格
  16. USB的EMI和ESD设计
  17. h5唤醒微信支付PHP,app内嵌微信h5支付,支付服务唤起支付处理
  18. i.MX6ULL嵌入式Linux开发4-根文件系统构建
  19. 妙法删除多余Windows XP管理员账户
  20. 基于椭圆模型的肤色检测

热门文章

  1. 将480*640rgb888彩色图像转为rgb444彩色图像并制作coe文件(matlab)
  2. 创建Ceph文件系统
  3. IP地址的概念和作用简析
  4. GCC编译器原理 1.3------GCC 工具:gprof、ld、libbfd、libiberty 和libopcodes
  5. 全面注册制对量化交易的影响 | A+CLUB 2023专题峰会圆桌论坛
  6. 技术栈 | 开源社区的明星项目—Ceph谈
  7. 肝了一个月!这本 Java 开发手册出炉啦!
  8. mt6735 [Speech App]使用velcom卡呼叫其他运营商号码回铃声叠加
  9. Pr 入门教程:如何隔离颜色?
  10. 长链接和短链接的区别?为什么要使用短链接?