先看效果圖:
自定義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.線程的謹慎使用