前言
- 本篇文章记录通过自定义View实现Android下可滑动的进度条
- 学习巩固自定义View知识
说明
1、实现效果
文中实现的效果都是未加抗锯齿
2、View绘制解析
上图自定义View中有文本(大小)
、背景条(灰色)
、进度条(绿色)
、滑动区域(白色内圆)
、外框圆(绿色)
、进度文字等元素,分析清晰元素的属性,代码更容易的去实现。
-
背景条属性:
起点坐标(startX)、长度(backgroundTotalLen)、颜色(backgroundColor)、线宽(backgroundStrokeW) -
进度条属性:
起点坐标(startX)、进度(progress)为0-100、颜色
(progressColor)、线宽(progressStrokeW) -
滑块内圆:
半径(handleRadius) -
外框圆:
这里设置半径比内圆大2f ,颜色和进度条颜色一致(progressColor) -
文本(进度值):
是否显示(showProgressText)
注意:
1、进度条的进度对应的坐标和滑动区域(内圆的坐标中心)、外框圆(坐标中心)一致
2、外框圆的颜色和进度条的颜色一致
3、绘制进度值文本如何与背景条与进度条保持垂直居中?
实现
上面分析了自定义View元素的属性值,下面代码进行实现
一、声明属性文件,在values下新建 attrs.xml文件
新建标签为 declare-styleable
类型的xml,名字SlideView
可自定义,一般和自定义View名保持一致,声明标签和名和对应的类型。
<resources>
<declare-styleable name="SlideView">
<!--进度背景颜色-->
<attr name="backgroundColor" format="color"/>
<!--背景的线宽-->
<attr name="backgroundStrokeW" format="float"/>
<!--背景总长度-->
<attr name="backgroundTotalLen" format="integer"/>
<!--起点x坐标-->
<attr name="startX" format="integer"/>
<!--进度-->
<attr name="progress" format="integer"/>
<!--进度颜色-->
<attr name="progressColor" format="color"/>
<!--进度线宽-->
<attr name="progressStrokeW" format="float"/>
<!--手柄圆半径-->
<attr name="handleRadius" format="float"/>
<!--是否显示文字进度-->
<attr name="showProgressText" format="boolean"/>
</declare-styleable>
</resources>
二、获取xml文件上的属性值,并给画笔设置属性
attributeSet
为Activity布局文件中声明的属性,R.styleable.SlideView
为attrs.xml
中声明的属性
/**
* 进度条监听,回调到外面
*/
private lateinit var listener:(progress:Int) -> Unit
fun onProgressChange(l:(progress:Int) ->Unit){
this.listener = l
}
/**
* 背景条
*/
private val backgroundPaint = Paint().apply {
style = Paint.Style.FILL
strokeCap = Paint.Cap.ROUND
}
/**
* 进度条画笔
*/
private val progressPaint = Paint().apply {
style = Paint.Style.FILL
strokeCap = Paint.Cap.ROUND
}
/**
* 内实心圆画笔
*/
private val innerCirclePaint = Paint().apply {
style = Paint.Style.FILL
strokeWidth = 10f
color = Color.WHITE
}
/**
* 外圆画笔
*/
private val outerCirclePaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 2f
}
/**
* 文字画笔
*/
private val textPaint = Paint().apply {
style = Paint.Style.FILL
textSize = 40f
color = Color.BLACK
}
init{
val ta = context.obtainStyledAttributes(attributeSet,`R.styleable.SlideView`)
//获取背景条颜色
backgroundColor = ta.getColor(R.styleable.SlideView_backgroundColor,context.getColor(R.color.colorEC))
//获取背景条线宽
backgroundStrokeW = ta.getFloat(R.styleable.SlideView_backgroundStrokeW,18f)
//获取背景条总长
totalLen = ta.getInt(R.styleable.SlideView_backgroundTotalLen,100)
//获取进度条颜色
progressColor = ta.getColor(R.styleable.SlideView_progressColor,context.getColor(R.color.colorGrassGreen))
//获取进度条线宽
progressStrokeW = ta.getFloat(R.styleable.SlideView_progressStrokeW,18f)
//获取进度条的进度
progress = ta.getInt(R.styleable.SlideView_progress,0)
//获取内圆半径
radius = ta.getFloat(R.styleable.SlideView_handleRadius,30.toFloat())
//获取绘制起点
startX = DeviceUtils.dp2px(context,ta.getInt(R.styleable.SlideView_startX,0).toFloat() + DeviceUtils.px2dp(context,radius) + 2f)
//是否显示进度值
showProgressText = ta.getBoolean(R.styleable.SlideView_showProgressText,true)
ta.recycle()
}
三、绘制元素
重写onDraw
方法,绘制背景条
、进度条
、滑动区域(内圆)
、外圆
/**
* 绘制元素
*/
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.apply {
//绘制背景条
drawLine(startX.toFloat(),endY ,endX.toFloat(),endY,backgroundPaint)
//绘制进度条
drawLine(startX.toFloat(),endY, progressValue,endY,progressPaint)
//绘制内圆
drawCircle(progressValue,endY, radius,innerCirclePaint)
//绘制外圆
drawCircle(progressValue,endY ,radius + 2f,outerCirclePaint)
//是否绘制进度值
if(showProgressText){
drawText("${(((progressValue - startX) / totalLen) * 100).toInt()} %", endX + 40f ,radius + 2f - baseLine,textPaint)
}
}
}
四、处理滑动事件
重写View
的OnTouchEvent
方法,需要判断手指按下的区域在外圆
的坐标值内,滑动的范围要限制在startX
和endX
之间
/**
* 处理拖动事件
*/
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
var cx = event.x
val cy = event.y
when(event.action){
MotionEvent.ACTION_DOWN ->{
//判断手指按下区域是否在句柄圆上, 左右和上下有效触摸区域扩大各40f
isOnTouch = (cx > progressValue - radius - 20f && cx < progressValue + radius + 20f && cy > -20f && cy < 2 * radius + 20f)
}
MotionEvent.ACTION_MOVE ->{
if (isOnTouch){
//限制最小值为起点startX
if(cx < startX){ cx = startX.toFloat() }
//限制最大值为终点endX
else if(cx > endX) { cx = endX.toFloat() }
progressValue = cx
//重新绘制
invalidate()
//将进度回调出去
listener.invoke(((progressValue / (endX - startX)) * 100).toInt())
}
}
MotionEvent.ACTION_UP ->{
isOnTouch = false
}
}
return true
}
五、问题点的处理
绘制进度文字时遇到一个问题,就是文字和背景条和进度条无法居中对齐。
if(showProgressText){
drawText("${(((progressValue - startX) / totalLen) * 100).toInt()} %", endX + 40f ,radius + 2f,textPaint)
}
进度值的X坐标在背景条后面,距离为40F
,Y坐标和内圆半径raduis + 外圆半径2F
,按理说应该是和滑动区域是垂直居中的。然后显示起来并没有居中,猜想文本在坐标系中的绘制较其有特殊。
那我们看下文本是怎么绘制在坐标系中的? 在Android中,提供了方法getTextBounds
查看文本的绘制区域。
/**
* 文字画笔
*/
private val textPaint = Paint().apply {
style = Paint.Style.FILL
textSize = 40f
color = Color.BLACK
}
rect = Rect()
textPaint.getTextBounds("100%",0,"100%".length,rect)
Log.d("AAAAAA","left = $left , top = ${rect?.top!!} , right = $right , bottom = ${rect?.bottom}")
输出:left = 0 , top = -29 , right = 0 , bottom = 2
通过打印出来的值,可以发现文本基线
并不是垂直于坐标轴Y轴的,当前Paint
和文本获取到绘制区域的 top
和bottom
值如下图所示,这就造成为了绘制后不垂直的原因。因为文本绘制基线涉及需要大量篇幅去说明,这里就不详细解释。那要如何处理呢?其实很简单,让文本的基线
垂直于Y轴即可。
解决方法:
把绘制文本的top
和bottom
取中间值作为垂直于Y轴的基线
带入计算即可
//文字绘制的基线
va baseLine = rect?.let {
(rect?.top!! + rect?.bottom!!)/ 2
}!!
Log.d("AAAAAA","baseLine = $baseLine")
输出: baseLine = 13
if(showProgressText){
drawText("${(((progressValue - startX) / totalLen) * 100).toInt()} %", endX + 40f ,radius + 2f - baseLine ,textPaint)
}
六、布局文件中使用
Xml中:
<com.xn.customview.widget.SlideView
android:id="@+id/svAlpha"
android:layout_width="@dimen/px_906"
android:layout_height="@dimen/px_72"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
app:backgroundColor="@color/colorEC"
app:backgroundStrokeW="18"
app:backgroundTotalLen="500"
app:handleRadius="30"
app:progress="50"
app:progressColor="@color/colorGrassGreen"
app:progressStrokeW="18"
app:showProgressText="true"
app:startX="0" />
Activity中:
//获取进度回调
mBinding.svAlpha.onProgressChange {
Log.d("AAAAAA","svAlpha progress = $it")
}
mBinding.svSize.onProgressChange {
Log.d("AAAAAA","svSize progress = $it")
}
结尾
文章中对文字位置处理不够完善,正确的处理方式请查看文章Android自定义控件(六) Andriod仿iOS控件Switch开关