最近開始深入學習自定義View,通過模仿學習,再配合Kotlin,寫了一些自定義控件,這次介紹的是類似于音樂播放暫停的一個控件
首先看一下效果圖:
下面先分析一下原理:
狀態1是播放狀態,有兩個小矩形,外面是一個圓,它需要最終變換成狀態3的暫停狀態
狀態2是兩個小矩形變成如圖的黑色三角的一個過程
我們可以通過動畫來實現它,兩個小矩形分別變成三角形的一半
同時再給畫布一個90度的旋轉
具體實現:
1.繼承View
1
|
class PlayPauseView : View |
2.重寫構造函數
1
2
3
4
5
|
constructor(context: Context?) : this (context, null ) constructor(context: Context?, attrs: AttributeSet?) : this (context, attrs, 0 ) constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super (context, attrs, defStyleAttr){ init(context!!,attrs!!) } |
一般的寫法都是講初始化代碼放在三個參數的構造函數里,其它兩個構造函數分別繼承自參數更多的一個本類的構造函數,所以這里用的是this關鍵字
3.初始化參數
首先我們需要先在values包的attrs文件中先聲明屬性
1
2
3
4
5
6
7
8
9
|
<declare-styleable name= "PlayPauseView" > <attr name= "barWidth" format= "dimension" /> <attr name= "barHeight" format= "dimension" /> <attr name= "barPadding" format= "dimension" /> <attr name= "barColor" format= "color" /> <attr name= "barBgColor" format= "color" /> <attr name= "barClockWise" format= "boolean" /> <attr name= "barPlayingState" format= "boolean" /> </declare-styleable> |
然后在構造函數中拿到這些參數
1
2
3
4
5
6
7
8
|
mBarWidth = typedArray.getDimension(R.styleable.PlayPauseView_barWidth, 10 * getDensity()) mBarHeight = typedArray.getDimension(R.styleable.PlayPauseView_barHeight, 30 * getDensity()) mPadding = typedArray.getDimension(R.styleable.PlayPauseView_barPadding, 10 * getDensity()) //可以通過上面的三個參數計算出下面的參數值,所以不再通過xml設置 mBarSpace = mBarHeight - mBarWidth * 2 mRadius = mBarWidth + mBarSpace.div( 2 ) + mPadding mWidth = mRadius * 2 mWidth = mRadius * 2 |
mBarWidth 是小矩形的寬度,mBarHeight 是小矩形的高度,mPadding 是小矩形距離整個view的邊界距離(參考上圖中狀態1中左邊小矩形距離大矩形的距離,距離top和left應該是一樣的,這個值就是mPadding )。
mBarSpace 是兩個小矩形之間的距離,mRadius 是狀態1中圓的半徑,mWidth 、mWidth 是狀態1中大矩形的寬高。(這些參數都是通過上面三個參數計算出來的)
同樣的在初始化這一步,初始化畫筆和兩個小矩形(半三角)Path
1
2
3
4
5
6
7
8
9
10
|
mBgPaint = Paint(Paint.ANTI_ALIAS_FLAG) mBgPaint!!.color = mBgColor mBgPaint!!.style = Paint.Style.FILL mBarPaint = Paint(Paint.ANTI_ALIAS_FLAG) mBarPaint!!.color = mBarColor mBarPaint!!.style = Paint.Style.FILL mLeftPath = Path() mRightPath = Path() |
同時通過動畫使矩形變成三角的參數 mProgress,在onDraw中會用到
4.測量控件
在onMeasure方法中測量控件的寬高,主要是在xml中wrap_content或者具體數值的時候
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super .onMeasure(widthMeasureSpec, heightMeasureSpec) val widthMode = MeasureSpec.getMode(widthMeasureSpec) val heightMode = MeasureSpec.getMode(heightMeasureSpec) val measureWidth = MeasureSpec.getSize(widthMeasureSpec) val measureHeight = MeasureSpec.getSize(heightMeasureSpec) when(widthMode){ MeasureSpec.EXACTLY ->{ mWidth = Math.min(measureWidth,measureHeight).toFloat() mHeight = Math.min(measureWidth,measureHeight).toFloat() setMeasuredDimension(mWidth.toInt(),mHeight.toInt()) } MeasureSpec.AT_MOST -> { mWidth = mRadius * 2 mHeight = mRadius * 2 setMeasuredDimension(mWidth.toInt(),mHeight.toInt()) } MeasureSpec.UNSPECIFIED -> { } } } |
5.繪制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
override fun onDraw(canvas: Canvas?) { super .onDraw(canvas) //需要重新設置,否則畫出來的圖形會保留上一次的 mLeftPath!!.rewind() mRightPath!!.rewind() mRadius = mWidth.div( 2 ) //先畫一個圓 canvas!!.drawCircle(mWidth.div( 2 ),mHeight.div( 2 ),mRadius,mBgPaint) //核心代碼 //順時針 if (isClockWise){ mLeftPath!!.moveTo(mPadding + (mBarWidth + mBarSpace.div( 2 )) * mProgress ,mPadding) mLeftPath!!.lineTo(mPadding ,mPadding + mBarHeight) mLeftPath!!.lineTo(mPadding + mBarWidth + mBarSpace.div( 2 ) * mProgress,mPadding + mBarHeight) mLeftPath!!.lineTo(mPadding + mBarWidth + mBarSpace.div( 2 ) * mProgress,mPadding) mLeftPath!!.close() mRightPath!!.moveTo(mPadding + mBarWidth + mBarSpace - mBarSpace.div( 2 ) * mProgress,mPadding) mRightPath!!.lineTo(mPadding + mBarWidth + mBarSpace - mBarSpace.div( 2 ) * mProgress,mPadding + mBarHeight) mRightPath!!.lineTo(mPadding + mBarWidth * 2 + mBarSpace ,mPadding + mBarHeight) mRightPath!!.lineTo(mPadding + mBarWidth * 2 + mBarSpace - (mBarWidth + mBarSpace.div( 2 )) * mProgress,mPadding) mRightPath!!.close() } //逆時針 else { mLeftPath!!.moveTo(mPadding,mPadding) mLeftPath!!.lineTo(mPadding + (mBarWidth + mBarSpace.div( 2 )) * mProgress,mPadding + mBarHeight) mLeftPath!!.lineTo(mPadding + mBarWidth + mBarSpace.div( 2 ) * mProgress,mPadding + mBarHeight) mLeftPath!!.lineTo(mPadding + mBarWidth + mBarSpace.div( 2 ) * mProgress,mPadding) mLeftPath!!.close() mRightPath!!.moveTo(mPadding + mBarWidth + mBarSpace - mBarSpace.div( 2 ) * mProgress,mPadding) mRightPath!!.lineTo(mPadding + mBarWidth + mBarSpace - mBarSpace.div( 2 ) * mProgress,mPadding + mBarHeight) mRightPath!!.lineTo(mPadding + mBarWidth * 2 + mBarSpace - (mBarWidth + mBarSpace.div( 2 )) * mProgress,mPadding + mBarHeight) mRightPath!!.lineTo(mPadding + mBarWidth * 2 + mBarSpace,mPadding) mRightPath!!.close() } var corner = 0 if (isClockWise){ corner = 90 } else { corner = - 90 } val rotation = corner * mProgress //旋轉畫布 canvas.rotate(rotation,mWidth.div( 2 ),mHeight.div( 2 )) canvas.drawPath(mLeftPath!!,mBarPaint) canvas.drawPath(mRightPath!!,mBarPaint) } |
通過這張圖來看一下核心代碼(順時針)
A點的坐標(mPadding + (mBarWidth + mBarSpace.div(2)) * mProgress ,mPadding)
mPadding 是小矩形距離大矩形的距離,A點最終會到F點,兩者相差一個矩形 + 兩個矩形間隔/2的距離(就是 mBarWidth + mBarSpace.div(2) 的距離),通過乘以一個從0到1的mProgress的變化即可
同理可得 D到F,B到E,C到E的變化坐標
右側的矩形也是如此計算,如果是逆時針旋轉,三角形是倒過來的,原理也是一樣的
6.動畫
上面提到過我們需要一個從0到1的mProgress的變化(從播放到暫停),或者需要一個從1到0的mProgress(從暫停到播放)
動畫核心代碼如下:
1
2
3
4
5
6
7
8
|
val valueAnimator = ValueAnimator.ofFloat( if (isPlaying) 1f else 0f, if (isPlaying) 0f else 1f) valueAnimator.duration = 200 valueAnimator.addUpdateListener { mProgress = it.animatedValue as Float invalidate() } return valueAnimator |
mProgress 不斷地變化,然后調用invalidate(),不斷地調用onDraw()方法
7.監聽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
setOnClickListener { if (isPlaying){ pause() mPlayPauseListener!!.pause() } else { play() mPlayPauseListener!!.play() } } private fun play() { getAnimator().cancel() setPlaying( true ) getAnimator().start() } private fun pause() { getAnimator().cancel() setPlaying( false ) getAnimator().start() } |
mPlayPauseListener是對外提供的接口,可以在Activity中拿到播放或者暫停的狀態,以供我們下一步的操作
8.使用
最后附上這個自定義View目前有的屬性:
1
2
3
4
5
6
7
|
app:barHeight= "30dp" //矩形條的寬度 app:barWidth= "10dp" //矩形條的高度 app:barPadding= "20dp" //矩形條距離原點(邊界)的距離 app:barClockWise= "true" //是否是順時針轉動 app:barPlayingState= "false" //默認的狀態,播放或者暫停 app:barBgColor= "@color/colorRed" //控件背景色 app:barColor= "@color/black" //按鈕顏色 |
在Activity或者Fragment中的使用:
1
2
3
4
5
6
7
8
9
10
11
12
|
val playPauseView = findViewById<PlayPauseView>(R.id.play_pause_view) //控件的點擊事件 playPauseView.setPlayPauseListener( this ) //需要實現的方法 override fun play() { Toast.makeText( this , "現在處于播放狀態" ,Toast.LENGTH_SHORT).show() } override fun pause() { Toast.makeText( this , "現在處于暫停狀態" ,Toast.LENGTH_SHORT).show() } |
至此,這個自定義View大致上完成了,還有一些細節就不再這里細說了。如果你有興趣深入了解,可以看一下這里:自定義View集合中的PlayPauseView,如果能隨手點個Star也是極好的。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/ckwccc/article/details/80761974