引言
今天玩小米mix2的時候看到了小米的時間控件效果真的很棒。有各種動畫效果,3d觸摸效果,然後就想着自己能不能也實現一個這樣的時間控件,那就行動繪製一個簡易版本的小米時間控件吧o((≧▽≦o)
效果圖
- 首先來看看小米的效果是這個樣子的
- 再來看看我的效果,感覺也差不多
具體實現過程
我們都知道自定控件的繪製有很多種,繼承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)
}
看下效果
繪製外邊緣4個文字
然後繪製4個文字刻度:3,6,9,12,文字繪製主要是要把握到文字的具體位置,這裏還是有點麻煩滴,你要知道具體的文字畫筆幾個屬性是什麼意思,如下圖
上代碼
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)
}
效果圖
繪製刻度
刻度就很簡單了,每個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()
}
}
如圖
繪製秒
這個是最有難度的一個地方,三角形通過旋轉畫布實現,知道秒錶走的角度就可以了
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()
}
}
如圖
繪製分針和時針
分針是一階貝塞爾就是畫線,時針是畫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)
}
繪製就結束了,接下來是如何讓時間走起來
如圖
讓時間走起來
我才用的方法就是直接採用延時任務的方式,每隔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>
這個控件首先要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地址