自定義view之kotlin繪製精簡小米時間控件

原文連接

引言

今天玩小米mix2的時候看到了小米的時間控件效果真的很棒。有各種動畫效果,3d觸摸效果,然後就想着自己能不能也實現一個這樣的時間控件,那就行動繪製一個簡易版本的小米時間控件吧o((≧▽≦o)

效果圖

  • 首先來看看小米的效果是這個樣子的
20180119151529508.png
  • 再來看看我的效果,感覺也差不多
aa.gif

具體實現過程

我們都知道自定控件的繪製有很多種,繼承view,繼承viewgroup,還有繼承已有的控件,但是無非就幾個步驟:

  • measure(): 測量,用來控制控件的大小,final 不建議複寫
  • layout(): 佈局,用來控制控件擺放的位置,繼承view不需要
  • draw(): 繪製,用來控制控件的樣子
  • ontouch();touch事件

其中最重要的就是draw()方法,當然如果涉及到touch事件,可以複寫ontouch方法進行處理,不過這個控件暫時用不了;
好吧,話不多說,開始理一下自己繪製的思路,我設想的繪製思路如下:

  • 1,先畫最外層的圓弧和文字
  • 2,再畫裏面刻度盤
  • 3,再畫秒錶三角形
  • 4,畫時針和分針
  • 5,畫中間小球
  • 6,3d觸摸效果

既然思路已經明確了,二話不說,那就開始搞吧

繪製前的準備工作

首先需要初始化各種畫筆,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)

    }

看下效果

c.png

繪製外邊緣4個文字

然後繪製4個文字刻度:3,6,9,12,文字繪製主要是要把握到文字的具體位置,這裏還是有點麻煩滴,你要知道具體的文字畫筆幾個屬性是什麼意思,如下圖

d.png

上代碼

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)
    }

效果圖

e.png

繪製刻度

刻度就很簡單了,每個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()
        }
    }

如圖

f.png

繪製秒

這個是最有難度的一個地方,三角形通過旋轉畫布實現,知道秒錶走的角度就可以了

   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()
        }
    }

如圖


g.png

繪製分針和時針

分針是一階貝塞爾就是畫線,時針是畫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)

    }

繪製就結束了,接下來是如何讓時間走起來
如圖

h.png

讓時間走起來

我才用的方法就是直接採用延時任務的方式,每隔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)
    }

3d觸效果

3d效果主要涉及到兩個類

一個是android.graphics.Camera:3D開發,不是android.hardware.Camera:別倒錯包了
還有一個是矩陣Matrix

一個照相機實例可以被用於計算3D變換,生成一個可以被使用的Matrix矩陣,一個實例,用在畫布上。
其實Camera內部機制實際上還是opengl,只不過大大簡化了使用。這樣有利於開發者進行開發

Camera的座標系是左手座標系。當手機平整的放在桌面上,X軸是手機的水平方向,Y軸是手機的豎直方向,Z軸是垂直於手機向裏的那個方向。

<div align=center>
1532659267(1).jpg

<div>

這個控件首先要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] * mMaxCameraRotate
        mCameraRotateY = percentArr[1] * mMaxCameraRotate
    }

    /**
     * 當撥動時鐘時,會發現時針、分針、秒針和刻度盤會有一個較小的偏移量,形成近大遠小的立體偏移效果
     * 一開始我打算使用 matrix 和 camera 的 mCamera.translate(x, y, z) 方法改變 z 的值
     */
    private fun getCanvasTranslate(event: MotionEvent) {
        val translateX = event.x - width / 2
        val translateY = event.y - height / 2
        //求出此時位移的大小與半徑之比
        val percentArr = getPercent(translateX, translateY)
        //最終位移的大小按比例勻稱改變
        mCanvasTranslateX = percentArr[0] * mMaxCanvasTranslate
        mCanvasTranslateY = percentArr[1] * mMaxCanvasTranslate
    }
        /**
     * 獲取一個操作旋轉或位移大小的比例
     * @return 裝有xy比例的float數組
     */
    private fun getPercent(x: Float, y: Float): FloatArray {
        val percentArr = FloatArray(2)
        var percentX = x / mRadius
        var percentY = y / mRadius
        if (percentX > 1) {
            percentX = 1f
        } else if (percentX < -1) {
            percentX = -1f
        }
        if (percentY > 1) {
            percentY = 1f
        } else if (percentY < -1) {
            percentY = -1f
        }
        percentArr[0] = percentX
        percentArr[1] = percentY
        return percentArr
    }

就這樣一個小米時間控件繪製完成了
當然我也傳了一個demo到github,如果有需要可以去下載玩玩
地址:github地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章