需求

UI同学出了一张图,其间一个ui是半圆环进度条,在2秒内从0%加载到100%。

Android 自定义一个半圆环进度条

相似这种,请不要介意它的难看,色彩能够调整,重要的是功能,功能,功能。

思路

说到进度条,首要想到的是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秒的时候截的图)

Android 自定义一个半圆环进度条

额外需求

是不是很完美了,基本上能够做到进度条客制化了,剩余的大多都是细节问题,如

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)更新画布,结果导致视图的布景变黑,但百分比仍是拥挤在一块。

Android 自定义一个半圆环进度条
百分比堆挤在一起

Android 自定义一个半圆环进度条
正常的百分比显现

进度告诉

这个简单,接口回调

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()
    }
}

以上便是本人完成一个简略的进度条的计划,能用,但用途不大,不过抵挡一般简略简单的需求很好满足。