之前的文章里我们已经介绍了自定义View相关的知识了,本篇文章我们就来实战一下,从零到一,实现一个环形进度条的控件。
我们说过,如果要实现一个自定义控件的话一般有两种继承方式:
如果要实现环形进度条的话,目前应该是没有系统已有的控件可以拓展,都需要较大程度的改动,所以我们直接继承View来实现即可。其次我们再来梳理一下要实现环形进度条的几个关键点:
这里我们就先不考虑warp_content,只考虑padding这个特殊情况:
//考虑padding之后的尺寸边界
val mSizeWithPadding
get() = RectF(0f+paddingLeft,0f+paddingTop,
width.toFloat()-paddingRight-paddingLeft,height.toFloat()-paddingBottom-paddingTop)
//绘制内容的宽度
private val contentWidth
get() = width-paddingLeft-paddingRight
//绘制内容的高度
private val contentHeight
get() = height-paddingTop-paddingBottom
我们先来介绍最重要的一点:如何绘制环形。这一片绘制view的内容我强烈建议大家可以去学习朱凯老师(扔物线)的课程,基本上涵盖了我们常用的绘制内容。
这里来简单介绍一下使用到的,Canavs绘制相关的API:
要实现环形进度条我们用这两个绘制方法就可以了。
既然是进度条那么就应该有当前进度值和最大进度值,这两个进度只要是为了确定在绘制的时候我们需要绘制弧度为多少的圆弧,我们将最大进度值设置为100,用以下代码表示:
class CircleLineWithText @JvmOverloads constructor(
mContext: Context, attributeSet: AttributeSet? = null, defStyle:Int = 0
):View(mContext,attributeSet,defStyle) {
....
//更新进度
fun updateProgress(progressIn100:Int) {
val tragetRad = progressIn100 * 360 / 100
currentRad = tragetRad.toFloat()
invalidate()
}
}
这里我定义了一个updateProgress方法来更新当前进度,我们都知道一圈圆为360度,所以说当前的目标弧度值为
(当前进度值 / 100) * 360 ,但是在整形中我们显然不能这么做,所以我们先乘以360再除以100;并在最后调用invalidate方法来触发View的重新绘制。
现在有了弧度值我们再来看具体的绘制方法(过程):
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//着色器 -- 辐射渐变
val shader = SweepGradient((paddingLeft+contentWidth/2-paddingRight).toFloat(),
(paddingTop+contentHeight/2-paddingBottom).toFloat(),
mLineColor,
Color.RED
)
mLinePaint.color = mLineColor
mLinePaint.shader = shader
//测试用的数据
Log.d(TAG, "onDraw: x:${contentWidth},y:${contentHeight},r:${min(contentWidth,contentHeight).toFloat()/2}")
//根据目标弧度值绘制弧形
canvas.drawArc(paddingLeft.toFloat()+mLinePaint.strokeWidth,
paddingTop.toFloat()+mLinePaint.strokeWidth,
paddingLeft+min(contentHeight,contentWidth).toFloat()-mLinePaint.strokeWidth-paddingRight,
paddingTop+min(contentHeight,contentWidth).toFloat()-mLinePaint.strokeWidth-paddingBottom,
0f,currentRad,false,mLinePaint
)
//绘制文字
mTextPaint.color = mTextColor
mTextPaint.textSize = mTextSize
mTextPaint.style = Paint.Style.FILL
mTextPaint.isUnderlineText = true
//需要显示在环形进度条中间的字符串
contentString = (currentRad/360*100).toInt().toString() + "%"
val lengthOfString = contentString.length * 25
canvas.drawText(contentString,
min(contentWidth,contentHeight).toFloat()/2-lengthOfString/2,
min(contentWidth,contentHeight).toFloat()/2+lengthOfString/10,
mTextPaint)
mTextPaint.style = Paint.Style.STROKE
//测试用
if (Debug) {
canvas.drawRect(mSizeWithPadding,mTextPaint)
canvas.drawRect(paddingLeft.toFloat()+mLinePaint.strokeWidth,
paddingTop.toFloat()+mLinePaint.strokeWidth,
paddingLeft+min(contentHeight,contentWidth).toFloat()-mLinePaint.strokeWidth-paddingRight,
paddingTop+min(contentHeight,contentWidth).toFloat()-mLinePaint.strokeWidth-paddingBottom,
mTextPaint)
}
}
这段就是绘制环形进度条的核心代码,实际上逻辑非常简单,主要就是我们需要对Canavs相关的API有所了解,其中关于绘制的线条的颜色,我使用到了shade着色器的辐射渐变模式,这样绘制的线条颜色就会随着绘制的位置改变。
class CircleLineWithText @JvmOverloads constructor(
mContext: Context, attributeSet: AttributeSet? = null, defStyle:Int = 0
):View(mContext,attributeSet,defStyle) {
companion object {
private const val TAG = "CircleLineWithText"
private const val Debug = true
}
var contentString = "Enjoy Your Life Cmf"
private val Color_TransParent
get() = resources.getColor(android.R.color.transparent)
var mTextColor = Color.BLACK
var mLineColor = Color.BLACK
var mTextSize = 18f
var currentRad = 0f
init {
//自定义属性
val typeArray = mContext.obtainStyledAttributes(attributeSet, R.styleable.CircleLineWithText)
mTextColor = typeArray.getColor(R.styleable.CircleLineWithText_text_Color,Color.BLACK)
mLineColor = typeArray.getColor(R.styleable.CircleLineWithText_line_Color,Color.BLACK)
mTextSize = typeArray.getFloat(R.styleable.CircleLineWithText_text_Size,18f)
currentRad = typeArray.getFloat(R.styleable.CircleLineWithText_current_Radius,0f)
}
private val mLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeCap = Paint.Cap.ROUND
style = Paint.Style.STROKE
strokeWidth = 30f
color = Color_TransParent
}
private val mTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color_TransParent
style = Paint.Style.STROKE
strokeWidth = 0f
}
val mSizeWithPadding
get() = RectF(0f+paddingLeft,0f+paddingTop,
width.toFloat()-paddingRight-paddingLeft,height.toFloat()-paddingBottom-paddingTop)
private val contentWidth
get() = width-paddingLeft-paddingRight
private val contentHeight
get() = height-paddingTop-paddingBottom
fun updateProgress(progressIn100:Int) {
val tragetRad = progressIn100 * 360 / 100
currentRad = tragetRad.toFloat()
invalidate()
}
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val shader = SweepGradient((paddingLeft+contentWidth/2-paddingRight).toFloat(),
(paddingTop+contentHeight/2-paddingBottom).toFloat(),
mLineColor,
Color.RED
)
mLinePaint.color = mLineColor
mLinePaint.shader = shader
Log.d(TAG, "onDraw: x:${contentWidth},y:${contentHeight},r:${min(contentWidth,contentHeight).toFloat()/2}")
canvas.drawArc(paddingLeft.toFloat()+mLinePaint.strokeWidth,
paddingTop.toFloat()+mLinePaint.strokeWidth,
paddingLeft+min(contentHeight,contentWidth).toFloat()-mLinePaint.strokeWidth-paddingRight,
paddingTop+min(contentHeight,contentWidth).toFloat()-mLinePaint.strokeWidth-paddingBottom,
0f,currentRad,false,mLinePaint
)
mTextPaint.color = mTextColor
mTextPaint.textSize = mTextSize
mTextPaint.style = Paint.Style.FILL
mTextPaint.isUnderlineText = true
contentString = (currentRad/360*100).toInt().toString() + "%"
val lengthOfString = contentString.length * 25
canvas.drawText(contentString,
min(contentWidth,contentHeight).toFloat()/2-lengthOfString/2,
min(contentWidth,contentHeight).toFloat()/2+lengthOfString/10,
mTextPaint)
mTextPaint.style = Paint.Style.STROKE
if (Debug) {
canvas.drawRect(mSizeWithPadding,mTextPaint)
canvas.drawRect(paddingLeft.toFloat()+mLinePaint.strokeWidth,
paddingTop.toFloat()+mLinePaint.strokeWidth,
paddingLeft+min(contentHeight,contentWidth).toFloat()-mLinePaint.strokeWidth-paddingRight,
paddingTop+min(contentHeight,contentWidth).toFloat()-mLinePaint.strokeWidth-paddingBottom,
mTextPaint)
}
}
}