Android 自定義View(一)實現時鐘錶盤效果

1、前言

Android 自定義View是高級進階不可或缺的內容,平常工做中,常常會遇到產品、UI設計出花裏胡哨的界面。當系統自帶的控件不能知足開發需求時,就只能本身動手擼一個效果。git

本文就帶自定義View初學者手動擼一個效果,經過自定義View實現鐘錶功能,每行代碼都有註釋,保證易懂,看不懂你留言打我!!!github

2、實現效果

一、先看效果圖

在這裏插入圖片描述
在這裏插入圖片描述

二、下載地址:點擊下載

三、步驟分析

實現以上效果,主要分爲四個步驟:web

  1. 繪製外層錶盤
  2. 繪製刻度線
  3. 繪製刻度數字
  4. 繪製指針

3、代碼實現

一、繪製外層錶盤

外層錶盤就是一個空心圓,只要獲取圓的x、y軸位置、圓的半徑,使用Canvas.drawCircle()方法便可完成。canvas

/**  * 繪製錶盤  */ private fun drawClock(canvas: Canvas, centerX: Float, centerY: Float) {   // 設置外層圓畫筆寬度  mPaint.strokeWidth = mCircleWidth  // 設置畫筆顏色  mPaint.color = Color.BLACK  // 設置畫筆空心風格  mPaint.style = Paint.Style.STROKE  // 繪製圓方法  canvas.drawCircle(centerX, centerY, radius, mPaint) } 複製代碼
在這裏插入圖片描述
在這裏插入圖片描述

二、繪製刻度線

繪製思路分析微信

看到效果圖上密密麻麻刻度線後,先不要着急上手,屢清楚繪製思路。繪製刻度線必定要結合Canvas幾何變換思路完成,千萬不要侷限於效果圖的表面(若是對Canvas相關API不熟悉的朋友,建議先了解下)。編輯器

  1. 假設以12點鐘爲例,那麼刻度線就是一條筆直的豎線,調用Canvas.drawLine()方法完成繪製。
  2. 若是每繪製完成一個刻度,把錶盤逆時針/順時針旋轉必定角度,將下次須要繪製刻度線的位置旋轉到12點鐘位置,那麼每次繪製刻度線的startX、startY、stopX、stopY一致(一致僅表明全部長刻度一致,全部短刻度一致)。
  3. 觀察錶盤共有60個刻度線(12長,48短),那麼每次旋轉角度 degrees=360/60

聽完以上分析,是否以爲繪製刻度線很簡單,只要在60個刻度遍歷判斷長短,便可輕鬆出效果。ide

/**  * 繪製錶盤刻度  */ private fun drawClockScale(canvas: Canvas, centerX: Float, centerY: Float) {  for (index in 1..60) {  // 刻度繪製以12點鐘爲準,每次將錶盤旋轉6°,後續繪製都以12點鐘爲基準繪製  canvas.rotate(6F, centerX, centerY)  // 繪製長刻度線  if (index % 5 == 0) {  // 設置長刻度畫筆寬度  mPaint.strokeWidth = 4.0F  // 繪製刻度線  canvas.drawLine(  centerX,  centerY - radius,  centerX,  centerY - radius + scaleMax,  mPaint  )  }  // 繪製短刻度線  else {  // 設置短刻度畫筆寬度  mPaint.strokeWidth = 2.0F  canvas.drawLine(  centerX,  centerY - radius,  centerX,  centerY - radius + scaleMin,  mPaint  )  }  } } 複製代碼

以上代碼就完成了繪製刻度線的效果,下面插個題外話,第一次嘗試在繪製刻度線的時候,錶盤數字一併完成,後來發現數字以下圖所示: 在這裏插入圖片描述佈局

// 測量繪製數字
mPaint.strokeWidth = 1.0F mPaint.style = Paint.Style.FILL mPaint.getTextBounds((index / 5).toString(), 0, (index / 5).toString().length, mRect) val width = mRect.width() canvas.drawText(  (index / 5).toString(),  centerX - mRect.width() / 2,  (centerY - radius + scaleMax + mRect.height() + 8),  mPaint ) 複製代碼

三、繪製數字方案

熱心網友指導我繪製數字新方案,真的是高手如雲阿。post

首先將座標位置(0,0)設置到圓心位置,這步是在繪製外層圓的時候,已經設置了。這樣的好處是後期減小不少計算的步驟,新方案已經在代碼中更改!學習

canvas.translate(centerX, centerY)
複製代碼

主要是經過canvas幾何變換方式,先將圓點平移到12點鐘位置,而後逆時針旋轉數字對應的角度,而後開始繪製數字文本。這樣的話,繪製數字文本就和繪製刻度線能夠一併完成,使得代碼清晰不少。 須要注意的是,記得在使用幾何變換先後分別調用canvas.restore()和canvas.restore()方法

其中相關座標計算方式:

一、平移 y 軸距離 = - 半徑 + 刻度線長度 + 刻度與文本間距 + 文本高度 / 2

(由於座標原點在圓心,須要平移到12點鐘位置,因此半徑爲負數)

二、旋轉角度 = - 6 * 數字大小

三、文本 x 軸距離 = 文本寬度 / 2 ;

四、文本 y 軸距離 = 文本高度 / 2 ;

附上繪製刻度線和文本的完整代碼:

/**  * 繪製錶盤刻度線和數字文本  */ private fun drawClockScale(canvas: Canvas) {  for (index in 1..60) {  // 刻度繪製以12點鐘爲準,每次將錶盤旋轉6°,後續繪製都以12點鐘爲基準繪製  canvas.rotate(6F, 0F, 0F)  // 繪製長刻度線  if (index % 5 == 0) {  // 設置長刻度畫筆寬度  mPaint.strokeWidth = 4.0F  // 繪製刻度線  canvas.drawLine(0F, -radius, 0F, -radius + scaleMax, mPaint)  /** 繪製文本 **/  canvas.save()  // 設置畫筆寬度  mPaint.strokeWidth = 1.0F  // 設置畫筆實心風格  mPaint.style = Paint.Style.FILL  mPaint.getTextBounds(  (index / 5).toString(),  0,  (index / 5).toString().length,  mRect  )  canvas.translate(0F, -radius + mNumberSpace + scaleMax + (mRect.height() / 2))  canvas.rotate((index * -6).toFloat())  canvas.drawText(  (index / 5).toString(), -mRect.width() / 2.toFloat(),  mRect.height().toFloat() / 2, mPaint  )  canvas.restore()  }  // 繪製短刻度線  else {  // 設置短刻度畫筆寬度  mPaint.strokeWidth = 2.0F  canvas.drawLine(0F, -radius, 0F, -radius + scaleMin, mPaint)  }  } } 複製代碼

四、繪製指針

指針繪製具體分如下步驟:

  1. 首先獲取當前時間
  2. 根據當前時間計算指針旋轉過的角度
  3. 利用Canvas.rotate()旋轉畫布
  4. 使用Canvas.drawRoundRect()繪製指針矩形
  5. 繪製圓點
/**  * 第四步:繪製指針  */ private fun drawPointer(canvas: Canvas, centerX: Float, centerY: Float) {  // 獲取當前時間:時分秒  val calendar = Calendar.getInstance()  val hour = calendar[Calendar.HOUR]  val minute = calendar[Calendar.MINUTE]  val second = calendar[Calendar.SECOND]  // 計算時分秒轉過的角度  val angleHour = (hour + minute.toFloat() / 60) * 360 / 12  val angleMinute = (minute + second.toFloat() / 60) * 360 / 60  val angleSecond = second * 360 / 60   // 繪製時針  canvas.save()  // 旋轉到時針的角度  canvas.rotate(angleHour, centerX, centerY)  val rectHour = RectF(  centerX - mHourPointWidth / 2,  centerY - radius / 2,  centerX + mHourPointWidth / 2,  centerY + radius / 6  )  // 設置時針畫筆屬性  mPaint.color = Color.BLUE  mPaint.style = Paint.Style.STROKE  mPaint.strokeWidth = mHourPointWidth  canvas.drawRoundRect(rectHour, mPointRange, mPointRange, mPaint)  canvas.restore()   // 繪製分針  canvas.save()  // 旋轉到分針的角度  canvas.rotate(angleMinute, centerX, centerY)  val rectMinute = RectF(  centerX - mMinutePointWidth / 2,  centerY - radius * 3.5f / 5,  centerX + mMinutePointWidth / 2,  centerY + radius / 6  )  // 設置分針畫筆屬性  mPaint.color = Color.BLACK  mPaint.strokeWidth = mMinutePointWidth  canvas.drawRoundRect(rectMinute, mPointRange, mPointRange, mPaint)  canvas.restore()   // 繪製秒針  canvas.save()  // 旋轉到分針的角度  canvas.rotate(angleSecond.toFloat(), centerX, centerY)  val rectSecond = RectF(  centerX - mSecondPointWidth / 2,  centerY - radius + 10,  centerX + mSecondPointWidth / 2,  centerY + radius / 6  )  // 設置秒針畫筆屬性  mPaint.strokeWidth = mSecondPointWidth  mPaint.color = Color.RED  canvas.drawRoundRect(rectSecond, mPointRange, mPointRange, mPaint)  canvas.restore()   // 繪製原點  mPaint.style = Paint.Style.FILL  canvas.drawCircle(  (centerX).toFloat(),  (centerY).toFloat(), mSecondPointWidth * 4, mPaint  ) } 複製代碼

以上就已經完成錶盤指針繪製工做,效果圖以下:

在這裏插入圖片描述
在這裏插入圖片描述

五、onMeasure測量View寬高

MeasureSpecMode有三個屬性:EXACTLY、AT_MOST、UNSPECIFIED

根據View在xml設置寬高類型不一樣會觸發相應的方法,這裏對onMeasure()測量不作具體講解,若是對自定義View測量寬高onMeasure()方法不熟悉的,請本身補習。onMeasure()方法也是自定義View學習過程當中很是重要的一個環節!

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
 super.onMeasure(widthMeasureSpec, heightMeasureSpec)  // 根據錶盤半徑 + 錶盤圓環寬度計算View寬度和高度  mWidth = onMeasuredSpec(widthMeasureSpec) + (mCircleWidth * 2).toInt()  mHeight = onMeasuredSpec(heightMeasureSpec) + (mCircleWidth * 2).toInt()  // 計算最終View錶盤的半徑  radius = (mWidth - mCircleWidth * 2) / 2  // 設置View最終寬高  setMeasuredDimension(mWidth, mHeight) } 複製代碼
private fun onMeasuredSpec(measureSpec: Int): Int {
 // 臨時值,用於計算後返回  var specViewSize = 0  val specMode = MeasureSpec.getMode(measureSpec)  val specSize = MeasureSpec.getSize(measureSpec)   when (specMode) {  MeasureSpec.EXACTLY -> {  // 通常爲固定尺寸或者match_parent  specViewSize = specSize  }  MeasureSpec.AT_MOST -> {  // 計算半徑以寬高最小值爲準  specViewSize = min((radius * 2).toInt(), specSize)  }  }  return specViewSize } 複製代碼

到此爲止,若是想讓錶盤和指針動起來,還須要在onDraw()方法裏調用postInvalidateDelayed(1000)方法,或者啓動一個線程,使每秒鐘刷新重繪一次佈局,這樣就可讓指針實時更新時間。最後貼上onDraw()方法裏面的代碼:

override fun onDraw(canvas: Canvas) {
 super.onDraw(canvas)   // 設置圓心X軸位置  val centerX: Float = (mWidth / 2).toFloat()  // 設置圓心Y軸位置  val centerY: Float = (mHeight / 2).toFloat()   /** 第一步:繪製最外層的圓 **/  drawClock(canvas, centerX, centerY)  /** 第二步:錶盤一共60個刻度,1到12點整數屬於長刻度,其他屬於短刻度 **/  drawClockScale(canvas, centerX, centerY)  /** 第三步:繪製錶盤數字 **/  drawClockNumber(canvas, centerX, centerY)  /** 第四步:繪製指針 **/  drawPointer(canvas, centerX, centerY)  // 刷新錶盤指針  postInvalidateDelayed(1000) } 複製代碼

總結

自定view在Android開發過程當中應用極其普遍,爲了更好的掌握,建議從自定義View繪製流程、Canvas、Paint、Path、onLayout()、onMeasure()、onDraw()系統化學習,而後上手多作練習,這樣勢必會對自定義View有很好的提高!

看完文章,是否是以爲這個效果其實也很簡單,案例中相關屬性可使用自定義屬性,由於練習案例,因此這裏在View中直接寫死了,感興趣的朋友可使用自定義屬性嘗試實現。這個案例基本上將自定義View中Canvas、Paint常見的API方法以及onMeasure()測量方法都應用到了,算是一個上手練習自定義View的好案例,但願看完文章對你學習有所幫助!

前文說過,保證每一個自定義View初學者都能看懂,由於每行代碼都會添加註釋,若是沒看懂的留言打我!!!

  • 歡迎加入微信交流羣,添加微信:jaynm888,拉你進羣
在這裏插入圖片描述
在這裏插入圖片描述
相關文章
相關標籤/搜索