需求
UI同学出了一张图,其中一个ui是半圆环进度条,在2秒内从0%加载到100%。
类似这种,请不要介意它的难看,颜色可以调整,重要的是功能,功能,功能。
思路
提到进度条,首先想到的是progressBar,不过progressBar只有水平进度条和圆环进度条这2种,不满足需求。
在网上找了一圈,有实现方案的,但感觉有点复杂,修改成自己的有点麻烦,遂决定自己写一个圆环,顺便重新学习一下canvas的使用。
Canvas
canvas的常用方法如下:
drawRect
public void drawRect(@android.annotation.NonNull android.graphics.RectF rect, @android.annotation.NonNull android.graphics.Paint paint)
public void drawRect(@android.annotation.NonNull android.graphics.Rect r, @android.annotation.NonNull android.graphics.Paint paint)
public void drawRect(float left, float top, float right, float bottom, @android.annotation.NonNull android.graphics.Paint paint)
绘制一个矩形区域,前面的参数是矩形的坐标,后面的参数是画笔。
drawPath
public void drawPath(@android.annotation.NonNull android.graphics.Path path, @android.annotation.NonNull android.graphics.Paint paint)
根据path路线绘制一条线,这个方法是灵活性最高的,但同时也是比较麻烦的,因为path是由一个个固定的关键点决定的。
drawLine
public void drawLine(float startX, float startY, float stopX, float stopY, @android.annotation.NonNull android.graphics.Paint paint)
public void drawLines(@android.annotation.NonNull float[] pts, int offset, int count, @android.annotation.NonNull android.graphics.Paint paint)
public void drawLines(@android.annotation.NonNull float[] pts, @android.annotation.NonNull android.graphics.Paint paint)
绘制一条线,灵活性同上
drawPoint
public void drawPoint(float x, float y, @android.annotation.NonNull android.graphics.Paint paint)
public void drawPoints(float[] pts, int offset, int count, @android.annotation.NonNull android.graphics.Paint paint)
public void drawPoints(@android.annotation.NonNull float[] pts, @android.annotation.NonNull android.graphics.Paint paint)
根据坐标绘制点,和上面的不同的是,点是孤立的,并非连接在一起的。
drawText
绘制文字,其中drawText有四种重载方法,和三种drawTextRun同名方法,2种drawTextOnPath。
drawOver
public void drawOval(@android.annotation.NonNull android.graphics.RectF oval, @android.annotation.NonNull android.graphics.Paint paint)
public void drawOval(float left, float top, float right, float bottom, @android.annotation.NonNull android.graphics.Paint paint)
根据rect的大小绘制椭圆
drawCircle
绘制圆形
public void drawCircle(float cx, float cy, float radius, @android.annotation.NonNull android.graphics.Paint paint)
根据圆心位置和半径绘制圆形
drawArc
public void drawArc(@android.annotation.NonNull android.graphics.RectF oval, float startAngle, float sweepAngle, boolean useCenter, @android.annotation.NonNull android.graphics.Paint paint)
public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @android.annotation.NonNull android.graphics.Paint paint)
根据区域、圆心坐标、半径绘制弧度,startAngle是圆弧的起始角度,sweepAngle是扫描的弧度。
1、方案一
半圆环,可以当初是两个两个圆弧拼接成的封闭区域,半径较大的圆弧的背景是圆弧的颜色,较小的背景是白色,这样就可以实现一个圆环。
2、方案二
方案一不推荐,因为实现带背景的半圆环进度条,一共需要画三个圆弧。一个圆弧,背景为灰色,一个绿色圆弧,大小同灰色圆弧,最后一个白色背景的小圆弧,计算起来比较麻烦。而且相比较两个圆环拼接,不如直接将paint的strokWidth设置成大圆弧和小圆弧的差。这样只需要绘制两个圆弧就行了,而且两个圆弧的大小一样,只是画笔的颜色不一样就行了。
实现
第一步 先实现一个半圆环
//目前先写死
private var ringWidth = 55f
private val paint by lazy {
Paint().apply {
isAntiAlias = true
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = ringWidth
}
}
//this is ring‘s background
private val greyPaint by lazy {
Paint().apply {
isAntiAlias = true
color = Color.parseColor("#999999")
style = Paint.Style.STROKE
strokeWidth = ringWidth
}
}
private var rectF: RectF = RectF()
用ringWidth表示圆环的宽度,paint是半圆环的颜色,greyPaint是背景色,rectF是绘制的区域。
然后在onMeasure中设置绘制区域的大小,在onDraw中绘制半圆环。
//在onMeasure中设置绘制区域
rectF.set(
ringWidth,
ringWidth,
measuredWidth.toFloat() - ringWidth,
(measuredHeight - ringWidth) * 2
)
//在onDraw中绘制半圆环进度条
//draw a ring background
canvas.drawArc(rectF, 180f, 180f, false, greyPaint)
//draw a ring
canvas.drawArc(rectF, 180f, 90f, false, paint)
这样子就能实现一个静止的半圆环进度条了。 那么,如何能让这个进度条动起来呢? drawArc方法中,sweepAngle是圆弧滑过的角度,只要在绘制的过程中不断更新这个值(假设时间是两秒,那么,在两秒的时间内,不断变化sweepAngle的值,然后更新ui)。
第二步 让半圆环进度条动起来
//圆弧的角度
private var sweepAngle = 0f
//动画时间
private var animatorDuration = 2000L
增加两个变量,然后添加动画
private val animator by lazy {
ValueAnimator.ofFloat(0f, 180f).apply {
addUpdateListener {
sweepAngle = it.animatedValue as Float
invalidate()
}
duration = animatorDuration
}
}
在构造函数中运行动画animator.start()
这样,一个会动的半圆环进度条就实现了。
第三步 定制化
让圆环进度条丰富起来。
改变进度条的颜色
这个进度条有两个颜色的值,一个是进度条的背景色,一个是进度条颜色。想要改变两个的值,有两种方法,一个是直接为其添加属性,另一个是加入两个暴露外部的方法。
添加属性
进度条在xml下的代码如下:
<com.testdemo.CustomHalfCircleProgress
android:id="@+id/progress"
android:layout_width="346dp"
android:layout_height="173dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
此时,在attrs文件下添加属性(可能values文件夹下面没有这个文件,那么也可以写在其他文件里,如themes或者在values文件夹下面创建一个文件attrs.xml)
<declare-styleable name="CustomHalfCircleProgress">
<attr name="bgColor" format="color" />
<attr name="progressColor" format="color" />
</declare-styleable>
这样,回到xml布局,就可以为progress添加这两个属性了
<com.light_mountain.testdemo.CustomHalfCircleProgress
android:id="@+id/progress"
android:layout_width="346dp"
android:layout_height="173dp"
app:bgColor="#3FB6F7"
app:progressColor="#006da8"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
回到CustomHalfCircleProgress自定义View中,在构造函数constructor(context: Context, attrs: AttributeSet)中获取定义的属性
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomHalfCircleProgress)
bgColor = typedArray.getColor(R.styleable.CustomHalfCircleProgress_bgColor, Color.RED)
progressColor =
typedArray.getColor(R.styleable.CustomHalfCircleProgress_progressColor, Color.BLUE)
typedArray.recycle()
paint.color = progressColor
bgPaint.color = bgColor
这样就可以实现一个以浅蓝色为背景,深蓝色为进度条颜色的半圆环进度条了。同理,其他的一些属性,如最大角度(最大值)、动画时间、进度条宽度等属性都能直接在xml文件中设置,方便快捷。
但它也有一个缺点。
因为是自定义属性,会占用属性名,而属性名是不能重复的,如果进度条使用了bgColor这个属性名,其他的方法就不能在使用这个属性名了。
添加方法
fun setBgColor(color: Int): CustomHalfCircleProgress {
bgColor = color
bgPaint.color = bgColor
return this
}
fun setProgressColor(color: Int): CustomHalfCircleProgress {
progressColor = color
paint.color = progressColor
return this
}
然后在onCreate中获取View,并设置颜色
val progress = findViewById<CustomHalfCircleProgress>(R.id.progress)
progress
.setProgressColor(Color.parseColor("#006DA8"))
.setBgColor(Color.parseColor("#3FB6F7"))
这样就可以随时更改进度条和背景的颜色。
补全其他的方法,如设置进度条的宽度,设置动画时间,启动动画和停止动画
fun setDuration(time: Long): CustomHalfCircleProgress {
this.animatorDuration = time
animator.duration = animatorDuration
return this
}
fun setRingWidth(value:Float) :CustomHalfCircleProgress{
this.ringWidth = value
paint.strokeWidth = ringWidth
bgPaint.strokeWidth = ringWidth
return this
}
fun start():CustomHalfCircleProgress {
animator.start()
return this
}
fun stop():CustomHalfCircleProgress{
animator.cancel()
return this
}
给进度条设置宽度为22f,动画时间为5秒,效果如下图(2秒的时候截的图)
额外需求
是不是很完美了,基本上可以做到进度条客制化了,剩下的大多都是细节问题,如
1、显示进度条的百分比
2、进度条结束了需要通知外部
3、自定义进度条的起始百分比和结束百分比
4、进度条渐变色
百分比
这两个也容易,增加一个变量percent表示当前的进度,再加入一个paint作为百分比的画笔,
private val textPaint = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#006da8")
textSize = 24f
textAlign = Paint.Align.CENTER
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
...省略代码...
//draw percent
canvas.drawText("${percent}%", percentX, percentY, textPaint)
}
percentX和percentY即在onMeasure中设置,我这边把文字的位置设置在水平居中,2/3高度的位置。
注:在canvas的画布中,x轴是向右为正,y轴是向下为正。
注2:刚开始绘制文字的时候,canvas的最后一个参数是直接使用进度条的paint,结果发现绘制出来的文字堆挤在一起,还以为是画布没有刷新,文字‘1%’和‘2%’重叠在一起导致的,于是使用canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)更新画布,结果导致视图的背景变黑,但百分比还是拥挤在一块。
百分比堆挤在一起
正常的百分比显示
进度通知
这个容易,接口回调
interface Listener {
fun success()
fun fail()
}
定义一个接口,两个方法,success表示进度条正常结束回调,fail表示进度条动画被cancel。
private val animator by lazy {
ValueAnimator.ofFloat(0f, 180f).apply {
...省略代码...
addListener(object : AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
isCancel = false
}
override fun onAnimationEnd(p0: Animator?) {
if (isCancel) listener?.fail() else listener?.success()
}
override fun onAnimationCancel(p0: Animator?) {
isCancel = true
}
override fun onAnimationRepeat(p0: Animator?) {
}
})
}
}
在progress控件绑定后,添加接口回调的方法
progress.setListener(object : CustomHalfCircleProgress.Listener{
override fun fail() {
//动画执行失败,进度条半道中止
}
override fun success() {
//进度条百分百,成功执行完毕
}
})
自定义起始百分比和结束百分比
这个更加容易了
//起始百分比
private var startAngle = 0f
//结束百分比
private var endAngle = 180f
...省略代码...
/**
* 这里要注意的是,startPercent<=endPercent,且数值在0-100的范围内,这里就不多做判断了
*/
fun setStartPercent(percent: Int): CustomHalfCircleProgress {
startAngle = percent * 180f / 100f
animator.setFloatValues(startAngle, endAngle)
return this
}
/**
* 这里需要注意的一点是,动画结束时,调用的仍然是listener?.success,即便此时未到100%,如果有需求的话,可以在onAnimationEnd里多做一层判断
*/
fun setEndPercent(percent: Int) :CustomHalfCircleProgress {
endAngle = percent * 180f / 100f
animator.setFloatValues(startAngle, endAngle)
return this
}
进度条渐变色
这个更加简单了,paint+渐变色直接可以搜得到答案 # Android绘图之LinearGradient线性渐变 就不提供相关代码了。
完整代码
以下是自定义的半圆环进度条的完整代码
class CustomHalfCircleProgress : View {
private var percent: Int = 0
private var percentX: Float = 0f
private var percentY: Float = 0f
private var ringWidth = 55f
//圆弧的角度
private var sweepAngle = 0f
private var startAngle = 0f
private var endAngle = 180f
//动画时间
private var animatorDuration = 2000L
private var bgColor = Color.parseColor("#999999")
private var progressColor = Color.GREEN
private var listener: Listener? = null
private var isCancel = false
private val paint =
Paint().apply {
isAntiAlias = true
color = Color.parseColor("#006da8")
style = Paint.Style.STROKE
strokeWidth = ringWidth
}
//this is ring‘s background
private val bgPaint =
Paint().apply {
isAntiAlias = true
color = Color.parseColor("#999999")
style = Paint.Style.STROKE
strokeWidth = ringWidth
}
private val textPaint = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#006da8")
textSize = 24f
textAlign = Paint.Align.CENTER
}
private val animator by lazy {
ValueAnimator.ofFloat(startAngle, endAngle).apply {
addUpdateListener {
sweepAngle = it.animatedValue as Float
percent = (sweepAngle * 100 / 180f).toInt()
postInvalidate()
}
duration = animatorDuration
addListener(object : AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
isCancel = false
}
override fun onAnimationEnd(p0: Animator?) {
if (isCancel) listener?.fail() else listener?.success()
}
override fun onAnimationCancel(p0: Animator?) {
isCancel = true
}
override fun onAnimationRepeat(p0: Animator?) {
}
})
}
}
private var rectF: RectF = RectF()//区域
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initView(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
private fun initView(context: Context, attrs: AttributeSet) {
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//draw a ring background
canvas.drawArc(rectF, 180f, 180f, false, bgPaint)
//draw a ring
canvas.drawArc(rectF, 180f, sweepAngle, false, paint)
//draw percent
canvas.drawText("${percent}%", percentX, percentY, textPaint)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
rectF.set(
ringWidth,
ringWidth,
measuredWidth.toFloat() - ringWidth,
(measuredHeight - ringWidth) * 2
)
percentX = measuredWidth / 2.0f
percentY = measuredHeight / 3.0f
}
fun setBgColor(color: Int): CustomHalfCircleProgress {
bgColor = color
bgPaint.color = bgColor
return this
}
fun setProgressColor(color: Int): CustomHalfCircleProgress {
progressColor = color
paint.color = progressColor
return this
}
fun setDuration(time: Long): CustomHalfCircleProgress {
this.animatorDuration = time
animator.duration = animatorDuration
return this
}
fun setRingWidth(value: Float): CustomHalfCircleProgress {
this.ringWidth = value
paint.strokeWidth = ringWidth
bgPaint.strokeWidth = ringWidth
return this
}
fun start(): CustomHalfCircleProgress {
animator.start()
return this
}
fun stop(): CustomHalfCircleProgress {
animator.cancel()
return this
}
fun setListener(listener: Listener): CustomHalfCircleProgress {
this.listener = listener
return this
}
/**
* 这里要注意的是,startPercent<=endPercent,且数值在0-100的范围内
*/
fun setStartPercent(percent: Int): CustomHalfCircleProgress {
startAngle = percent * 180f / 100f
animator.setFloatValues(startAngle, endAngle)
return this
}
/**
* 这里需要注意的一点是,动画结束时,调用的仍然是listener?.success,即便此时未到100%,如果有需求的话,可以在onAnimationEnd里多做一层判断
*/
fun setEndPercent(percent: Int) :CustomHalfCircleProgress {
endAngle = percent * 180f / 100f
animator.setFloatValues(startAngle, endAngle)
return this
}
interface Listener {
fun success()
fun fail()
}
}
以上就是本人实现一个简单的进度条的方案,能用,但用处不大,不过对付一般简单容易的需求很好满足。