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

1,680 阅读6分钟
原文链接: blog.csdn.net

引言

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

效果图

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

    这里写图片描述
  • 再来看看我的效果,感觉也差不多,只是少了一个触摸3d翻转效果

    这里写图片描述

具体实现过程

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

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

其中最重要的就是draw()方法,当然如果涉及到touch事件,可以复写ontouch方法进行处理,不过这个控件暂时用不了; 好吧,话不多说,开始理一下自己绘制的思路,我设想的绘制思路如下:

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

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

绘制前的准备工作

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

init {
        mPaintOutCircle.color = color_halfWhite
        mPaintOutCircle.strokeWidth = dp2px(1f)
        mPaintOutCircle.style = Paint.Style.STROKE

        mPaintOutText.color = color_halfWhite
        mPaintOutText.strokeWidth = dp2px(1f)
        mPaintOutText.style = Paint.Style.STROKE
        mPaintOutText.textSize = sp2px(10f).toFloat()
        mPaintOutText.textAlign = Paint.Align.CENTER

        mPaintProgressBg.color = color_halfWhite
        mPaintProgressBg.strokeWidth = dp2px(2f)
        mPaintProgressBg.style = Paint.Style.STROKE

        mPaintProgress.color = color_halfWhite
        mPaintProgress.strokeWidth = dp2px(2f)
        mPaintProgress.style = Paint.Style.STROKE

        mPaintTriangle.color = color_white
        mPaintTriangle.style = Paint.Style.FILL

        mPaintHour.color = color_halfWhite
        mPaintHour.style = Paint.Style.FILL

        mPaintMinute.color = color_white
        mPaintMinute.strokeWidth = dp2px(3f)
        mPaintMinute.style = Paint.Style.STROKE
        mPaintMinute.strokeCap = Paint.Cap.ROUND

        mPaintBall.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 height
        setMeasuredDimension(imageSize, imageSize)
    }

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

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

  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mWidth = w
        mHeight = h
        mCenterX = mWidth / 2
        mCenterY = 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) / 2
        val 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) / 2
        for (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) / 2
        for (i in 0..90 step 2) {
            //第一个参数设置透明度,实现渐变效果,从255到0
            canvas.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 * 30
        mMinuteDegress = minute * 6
        mSecondMillsDegress = second * 6 + secondMills * 0.006f
        mSecondDegress = second * 6
        val 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)
    }

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