[Android View]实现一个环形进度条
导言
之前的文章里我们已经介绍了自定义View相关的知识了,本篇文章我们就来实战一下,从零到一,实现一个环形进度条的控件。
具体实现
大体框架
我们说过,如果要实现一个自定义控件的话一般有两种继承方式:
- 继承View:重写onDraw,还需要支持warp_content等属性。
- 继承系统已有控件:重写onDraw 或者绘制中的其他方法,一般用于拓展已有控件的功能。
如果要实现环形进度条的话,目前应该是没有系统已有的控件可以拓展,都需要较大程度的改动,所以我们直接继承View来实现即可。其次我们再来梳理一下要实现环形进度条的几个关键点:
- 绘制环形(根据进度,显示的颜色)
- 支持warp_content属性和padding属性
实际上也并不难,主要就是根据进度的不同绘制环形这一步,完成这一步,环形进度条也大致完毕。
确定尺寸
这里我们就先不考虑warp_content,只考虑padding这个特殊情况:
//考虑padding之后的尺寸边界
val mSizeWithPaddingget() = RectF(0f+paddingLeft,0f+paddingTop,width.toFloat()-paddingRight-paddingLeft,height.toFloat()-paddingBottom-paddingTop)
//绘制内容的宽度
private val contentWidthget() = width-paddingLeft-paddingRight
//绘制内容的高度
private val contentHeightget() = 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 / 100currentRad = 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 = mLineColormLinePaint.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 = mTextColormTextPaint.textSize = mTextSizemTextPaint.style = Paint.Style.FILLmTextPaint.isUnderlineText = true//需要显示在环形进度条中间的字符串contentString = (currentRad/360*100).toInt().toString() + "%"val lengthOfString = contentString.length * 25canvas.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_TransParentget() = resources.getColor(android.R.color.transparent)var mTextColor = Color.BLACKvar mLineColor = Color.BLACKvar mTextSize = 18fvar currentRad = 0finit {//自定义属性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.ROUNDstyle = Paint.Style.STROKEstrokeWidth = 30fcolor = Color_TransParent}private val mTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {color = Color_TransParentstyle = Paint.Style.STROKEstrokeWidth = 0f}val mSizeWithPaddingget() = RectF(0f+paddingLeft,0f+paddingTop,width.toFloat()-paddingRight-paddingLeft,height.toFloat()-paddingBottom-paddingTop)private val contentWidthget() = width-paddingLeft-paddingRightprivate val contentHeightget() = height-paddingTop-paddingBottomfun updateProgress(progressIn100:Int) {val tragetRad = progressIn100 * 360 / 100currentRad = 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 = mLineColormLinePaint.shader = shaderLog.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 = mTextColormTextPaint.textSize = mTextSizemTextPaint.style = Paint.Style.FILLmTextPaint.isUnderlineText = truecontentString = (currentRad/360*100).toInt().toString() + "%"val lengthOfString = contentString.length * 25canvas.drawText(contentString,min(contentWidth,contentHeight).toFloat()/2-lengthOfString/2,min(contentWidth,contentHeight).toFloat()/2+lengthOfString/10,mTextPaint)mTextPaint.style = Paint.Style.STROKEif (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)}}
}