最近开始深入学习自定义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