android自定义view实现一个钟表

先看效果图:

 

自定义view大家肯定已经不陌生了,所以直接今天直接步入正题:如何利用canvas去绘制出一个钟表

当然绘制之前我们必须进行测量(重写onMeasure),根据自己的规则去测量,这暂时是将控件限制为一个正方形。

首先我们先把钟表分解,看它由哪几部分组成。如上图:钟表包括表盘(刻度)和表针还有文字构成。

分清结构之后我们再明确canvas需要画什么,表盘的构成其实就是外层一个圆,然后上面是有规律的线段,表针就是三个长短不一的线段,再加上12个钟点文字。这样一分析是不是发现调用canvas的drawCircle、drawLine和drawText就可以完成钟表的绘制了。

既然明确了我们绘制所需要的方法,那么就开始重头戏了,告诉canvas在哪绘制这些零件。

最外层的圆是最简单的,我们只需要以控件的中心为圆心,控件的宽度一半为半径画一个圆就可以了。

接下来就是难点一了,这些刻度怎么办呢,其实我们不难发现其中的规律,每个刻度之间的弧度是一样的,那这样我们是不是可以通过旋转画布就可以实现这些刻度的绘制呢,答案是肯定的。

难点二,文字又该如何绘制,难道也通过旋转画布吗,但是你想一下,假如通过旋转画布去绘制文字,那有些文字可是会颠倒的,这并不是我们想要的结果,那该怎么办,这时候我们只能通过数学计算老老实实的计算每个文字的起始座标,这些座标并没有想象中的复杂,我们可以根据中心点的位置和偏移角度(当然还需要考虑文字的宽度)算出。

难点三,绘制表针,其实文字绘制出来,那么同样可以根据中心点和偏移角度算出表针的起始座标和结束座标
表心就是一个实体的圆,这个就简单了。

好像还没说时分秒是怎么确定的,这当然是通过系统时间获取的了。说到这里似乎一个静态钟表已经绘制出来了,接下来让它动起来就可以了。在这我们启动一个线程,让它隔一秒钟进行一次重绘即可。

下面我直接贴一下代码把,代码是用kotlin实现(这不是重点)的

package com.example.commonui.widget

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.os.Handler
import android.os.Message
import android.util.AttributeSet
import android.view.View
import java.util.*

/**
 * Created by zhang on 2017/12/20.
 */
class ClockView(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {

    companion object {
        private const val DEFAULT_WIDTH = 200 //默认宽度
    }

    private lateinit var mBlackPaint: Paint//黑色画笔
    private lateinit var mRedPaint: Paint //红色画笔
    private lateinit var mBlackPaint2: Paint//黑色画笔
    private lateinit var mTextPaint: Paint
    private var hour: Int? = null
    private var minute: Int? = null
    private var second: Int? = null
    private val textArray = arrayOf("12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11")
    private var refreshThread: Thread? = null
    private var mHandler = @SuppressLint("HandlerLeak")
    object : Handler() {
        override fun handleMessage(msg: Message?) {
            super.handleMessage(msg)
            when (msg?.what) {
                0 -> {
                    invalidate()
                }
            }

        }
    }

    init {
        initPaints()
    }

    /**
     * 初始化画笔
     */
    private fun initPaints() {
        mBlackPaint = Paint()
        with(mBlackPaint) {
            color = Color.BLACK
            strokeWidth = 5f
            isAntiAlias = true
            style = Paint.Style.STROKE
        }
        //用于画表心
        mBlackPaint2 = Paint()
        with(mBlackPaint2) {
            color = Color.BLACK
            isAntiAlias = true
            style = Paint.Style.FILL
        }
        mRedPaint = Paint()
        with(mRedPaint) {
            color = Color.RED
            strokeWidth = 5f
            isAntiAlias = true
        }

        mTextPaint = Paint()
        with(mTextPaint) {
            color = Color.BLACK
            textSize = 30f
            isAntiAlias = true
        }
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //获取当前时间
        getCurrentTime()

        //先画最外层的圆圈
        drawOuterCircle(canvas)

        //画刻度
        drawScale(canvas)

        //绘制文字
        drawTimeText(canvas)

        //绘制表针
        drawHand(canvas)

        //绘制表心
        drawCenter(canvas)
    }

    private fun getCurrentTime() {
        val calendar = Calendar.getInstance()
        hour = calendar.get(Calendar.HOUR)
        minute = calendar.get(Calendar.MINUTE)
        second = calendar.get(Calendar.SECOND)
    }

    private fun drawOuterCircle(canvas: Canvas?) {
        mBlackPaint.strokeWidth = 5f
        canvas?.drawCircle(measuredWidth / 2.toFloat(), measuredHeight / 2.toFloat(), (measuredWidth / 2 - 5).toFloat(), mBlackPaint)
    }

    private fun drawCenter(canvas: Canvas?) {
        canvas?.drawCircle(measuredWidth / 2.toFloat(), measuredHeight / 2.toFloat(), 20f, mBlackPaint2)
    }

    private fun drawHand(canvas: Canvas?) {
        drawSecond(canvas, mRedPaint)
        mBlackPaint.strokeWidth = 10f
        drawMinute(canvas, mBlackPaint)
        mBlackPaint.strokeWidth = 15f
        drawHour(canvas, mBlackPaint)
    }

    private fun drawTimeText(canvas: Canvas?) {
        val textR = (measuredWidth / 2 - 50).toFloat()//文字构成的圆的半径
        for (i in 0..11) {
            //绘制文字的起始座标
            val startX = (measuredWidth / 2 + textR * Math.sin(Math.PI / 6 * i) - mTextPaint.measureText(textArray[i]) / 2).toFloat()
            val startY = (measuredHeight / 2 - textR * Math.cos(Math.PI / 6 * i) + mTextPaint.measureText(textArray[i]) / 2).toFloat()
            canvas?.drawText(textArray[i], startX, startY, mTextPaint)
        }
    }

    private fun drawScale(canvas: Canvas?) {
        var scaleLength: Float?
        canvas?.save()
        //0..59代表[0,59]
        for (i in 0..59) {
            if (i % 5 == 0) {
                //大刻度
                mBlackPaint.strokeWidth = 5f
                scaleLength = 20f
            } else {
                //小刻度
                mBlackPaint.strokeWidth = 3f
                scaleLength = 10f
            }
            canvas?.drawLine(measuredWidth / 2.toFloat(), 5f, measuredWidth / 2.toFloat(), (5 + scaleLength), mBlackPaint)
            canvas?.rotate(360 / 60.toFloat(), measuredWidth / 2.toFloat(), measuredHeight / 2.toFloat())
        }
        //恢复原来状态
        canvas?.restore()
    }

    /**
     * 绘制秒针
     */
    private fun drawSecond(canvas: Canvas?, paint: Paint?) {
        //秒针长半径 (表针会穿过表心 所以需要根据两个半径计算起始和结束半径)
        val longR = measuredWidth / 2 - 60
        val shortR = 60
        val startX = (measuredWidth / 2 - shortR * Math.sin(second!!.times(Math.PI / 30))).toFloat()
        val startY = (measuredWidth / 2 + shortR * Math.cos(second!!.times(Math.PI / 30))).toFloat()
        val endX = (measuredWidth / 2 + longR * Math.sin(second!!.times(Math.PI / 30))).toFloat()
        val endY = (measuredWidth / 2 - longR * Math.cos(second!!.times(Math.PI / 30))).toFloat()
        canvas?.drawLine(startX, startY, endX, endY, paint)
    }

    /**
     * 绘制分针
     */
    private fun drawMinute(canvas: Canvas?, paint: Paint?) {
        //半径比秒针小一点
        val longR = measuredWidth / 2 - 90
        val shortR = 50
        val startX = (measuredWidth / 2 - shortR * Math.sin(minute!!.times(Math.PI / 30))).toFloat()
        val startY = (measuredWidth / 2 + shortR * Math.cos(minute!!.times(Math.PI / 30))).toFloat()
        val endX = (measuredWidth / 2 + longR * Math.sin(minute!!.times(Math.PI / 30))).toFloat()
        val endY = (measuredWidth / 2 - longR * Math.cos(minute!!.times(Math.PI / 30))).toFloat()
        canvas?.drawLine(startX, startY, endX, endY, paint)
    }


    /**
     * 绘制时针
     */
    private fun drawHour(canvas: Canvas?, paint: Paint?) {
        //半径比秒针小一点
        val longR = measuredWidth / 2 - 120
        val shortR = 40
        val startX = (measuredWidth / 2 - shortR * Math.sin(hour!!.times(Math.PI / 6))).toFloat()
        val startY = (measuredWidth / 2 + shortR * Math.cos(hour!!.times(Math.PI / 6))).toFloat()
        val endX = (measuredWidth / 2 + longR * Math.sin(hour!!.times(Math.PI / 6))).toFloat()
        val endY = (measuredWidth / 2 - longR * Math.cos(hour!!.times(Math.PI / 6))).toFloat()
        canvas?.drawLine(startX, startY, endX, endY, paint)
    }

    /**
     * 进行测量
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
        val heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
        val result = if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            DEFAULT_WIDTH
        } else {
            Math.min(widthSpecSize, heightSpecSize)
        }

        setMeasuredDimension(result, result)
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        //启动线程 刷新界面
        refreshThread = Thread(Runnable {
            while (true) {
                try {
                    Thread.sleep(1000)
                    mHandler.sendEmptyMessage(0)
                } catch (e: InterruptedException) {
                    break
                }
            }
        })
        refreshThread?.start()
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        mHandler.removeCallbacksAndMessages(null)
        //中断线程
        refreshThread?.interrupt()
    }
}


在这送上几点建议,1.尽量不要再ondraw里面创建对象,因为view可能会多次重绘,每次都创建新的对象会造成不必要的内存浪费

 

2.onmeasure方法会调用多次,请保证你的逻辑覆盖性,否则可能会出现没有按照你的预期得到宽高

3.线程的谨慎使用

 

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