1. 程式人生 > 程式設計 >android自定義view實現鐘錶效果

android自定義view實現鐘錶效果

本文例項為大家分享了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(),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(),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,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,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,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.執行緒的謹慎使用

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。