Android 以圆形进度条为例讲怎么实现自定义View

关键词:自定义View,圆形进度条,新手教学向


开篇想先问下,Android 这个行业还有新人进来学习吗∠( ᐛ 」∠)_


以图为例,看到下图你第一个想到的实现方案是什么?

环形多进度条

是不是想写三个 ProgressBar?

然后产品经理说,这个地方需要有两点需要注意

  1. 进度在 >100% 和 <100% 的时候效果不同
  2. 根据用户角色不同,这地方有时候存在展示2~4个进度环的可能

这时候还考虑叠 ProgressBar 吗?

仔细分析一下,这个控件其实并不复杂,会画弧线,会画圈就行。

        // 圆环
        canvas.drawCircle(...)
        // 画弧线
        canvas.drawArc(...)

那么,再把这个控件再拆分一下,由Drawable绘制圆形进度条,View负责控制Drawable数量和颜色,这样就OK了。

设计完成,开始实现


进度条Drawable

可以直接看 draw 方法


/**
 * 构造方法传入背景颜色、进度条颜色、背景环宽度、进度条宽度
 */
class CircleProgressDrawable(
    val bgColor: Int = Color.parseColor("#1F000000"),
    val pColor: Int = Color.RED,
    val bgWidth: Float = 8F,
    val pWidth: Float = 8F
) : Drawable() {

    /**
     * 默认值和常用值,最好用常量保存
     * 就算没有用常量保存,默认值也最好写在变量初始化位置
     */
    companion object {
        /**
         * 默认圆环阴影绘制长度,占整体圆环的百分比
         */
        private const val DEFAULT_SHADOW_DRAW_PERCENT = 0.08F

        /**
         * 阴影默认启示渐变色
         */
        private const val DEFAULT_SHADOW_START_COLOR: String = "#70000000"
    }

    /**
     * 当前进度
     */
    var progress: Int = 0

    /**
     * 总进度
     */
    var total: Int = 0

    /**
     * 阴影绘制角度
     */
    @FloatRange(from = 0.0, to = 1.0)
    var shadowPercent: Float = DEFAULT_SHADOW_DRAW_PERCENT

    private var width = 0
    private var height = 0
    private var centerX = 0
    private var centerY = 0

    /**
     * 圆环半径
     */
    private var radius = 0F

    /**
     * 绘制角度
     */
    private var drawPercent = 0F
        set(value) {
            field = value
            // 重新设置绘制角度时,阴影渐变色也要更改
            mShadowShader = SweepGradient(
                0F,
                0F,
                intArrayOf(Color.parseColor(DEFAULT_SHADOW_START_COLOR), Color.TRANSPARENT),
                floatArrayOf(
                    0F,
                    shadowPercent
                )
            )
        }

    /**
     * 背景绘制 Paint
     */
    private val mBgPaint: Paint = Paint().apply {
        color = bgColor
        style = Paint.Style.STROKE
        strokeWidth = bgWidth
    }

    /**
     * 进度绘制 Paint
     */
    private val mPaint: Paint = Paint().apply {
        color = pColor
        style = Paint.Style.STROKE
        strokeWidth = pWidth
        strokeCap = Paint.Cap.ROUND
    }

    /**
     * overSize 场景下,阴影绘制
     */
    private val mShadowPaint: Paint = Paint().apply {
        strokeWidth = pWidth
        style = Paint.Style.STROKE
        strokeCap = Paint.Cap.SQUARE
    }

    /**
     * overSize 场景下,阴影的渐变色
     *
     * 如果设置阴影角度,渐变色会自动调整
     */
    var mShadowShader = SweepGradient(
        0F,
        0F,
        intArrayOf(Color.parseColor(DEFAULT_SHADOW_START_COLOR), Color.TRANSPARENT),
        floatArrayOf(
            0F,
            shadowPercent
        )
    )
   
    /**
     * draw 是一个高频调用方法,尤其是动画过程中,
     * 所以尽量保证 draw 里头不要去初始化对象,
     * 这样能有效防止内存抖动
     */ 
    override fun draw(canvas: Canvas) {
        width = bounds.width()
        height = bounds.height()
        // 中心点坐标
        centerX = (bounds.left + bounds.right) / 2
        centerY = (bounds.top + bounds.bottom) / 2
        // 半径
        radius = bounds.width() / 2 - bgWidth.coerceAtLeast(pWidth) / 2

        // 将坐标系平移到中心点,然后将0度位置朝上,
        // 不然坐标系在左上角,0度位置是右方,这样写代码会更难
        canvas.save()
        canvas.translate(centerX.toFloat(), centerY.toFloat())
        canvas.rotate(-90F)

        // 绘制背景圆环,固定绘制在最底下
        canvas.drawCircle(0F, 0F, radius, mBgPaint)

        // 计算绘制角度
        drawPercent = (progress % total.coerceAtLeast(1)).toFloat() / total.coerceAtLeast(1)

        // 绘制进度条,有0%,0~100%,100%,>100%四种样式,要分开处理
        if (progress == 0) {
            // 没有进度绘制点
            canvas.drawPoint(radius, 0F, mPaint)
        } else if (progress in 0 until total) {
            // 普通进度正常绘制弧线
            canvas.drawArc(
                -radius,
                -radius,
                +radius,
                +radius,
                0F,
                360F * drawPercent,
                false,
                mPaint
            )
        } else if (progress == total) {
            // 满进度直接画圈
            canvas.drawCircle(0F, 0F, radius, mPaint)
        } else if (progress > total) {
            // 进度大于100% 先画圈,然后绘制阴影,最后绘制一段进度条压在阴影上
            canvas.drawCircle(0F, 0F, radius, mPaint)

            canvas.save()
            // 旋转,是因为 SweepShader 的渐变是从 0度角度开始
            canvas.rotate(360F * drawPercent)
            canvas.drawArc(
                -radius,
                -radius,
                +radius,
                +radius,
                0F,
                360F * shadowPercent,
                false,
                mShadowPaint.apply {
                    shader = mShadowShader
                }
            )
            canvas.restore()

            // 绘制一段进度条压在阴影上
            canvas.drawArc(
                -radius,
                -radius,
                +radius,
                +radius,
                0F,
                360F * drawPercent,
                false,
                mPaint
            )
        }

        // canvas 的 save 和 restore 必须配对,这点不能忘
        canvas.restore()
    }

    override fun setAlpha(alpha: Int) {
        mPaint.alpha = alpha
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        mPaint.colorFilter = colorFilter
    }

    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}

这样,一个单独的进度条 drawable 就完成了,支持自定义背景宽度颜色、进度条宽度颜色。


多进度环形进度条 MultiCircleProgressView

Drawable 已经处理了进度条的绘制,那么 View 就只需要控制 Drawable 数量和间距了

p.s. 这里基类不应该用 FrameLayout 的,最初设计是环形进度条 View,而不是 Drawable导致的问题。


class MultiCircleProgressView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    /**
     * 进度条间距
     * 默认 2dp
     */
    var dividerWidth: Float = dp2px(2F)

    /**
     * 进度条默认宽度
     * 默认 8dp
     */
    var progressWidth: Float = dp2px(8F)

    /**
     * 进度条默认背景宽度
     * 默认 8dp
     */
    var progressBgWidth: Float = dp2px(8F)

    /**
     * 进度条默认颜色
     */
    var progressColor: Int = Color.parseColor("#E03810")

    /**
     * 进度条默认背景颜色
     */
    var progressBgColor: Int = Color.parseColor("#16E03810")

    /**
     * 保存要绘制的进度条信息
     */
    private val progressList = mutableListOf<CircleProgressDrawable>()

    /**
     * 添加进度条
     *
     * @param progress 当前进度,默认 0
     * @param total 总进度,默认 100
     * @param color 进度条颜色,不传取默认颜色
     * @param width 进度条宽度,不传取默认宽度
     * @param backgroundColor 进度条背景颜色,不传取默认颜色
     * @param backgroundWidth 进度条背景宽度,不传取默认宽度
     */
    fun addProgress(
        progress: Int = 0,
        total: Int = 100,
        color: Int = progressColor,
        width: Float = progressWidth,
        backgroundColor: Int = progressBgColor,
        backgroundWidth: Float = progressBgWidth,
    ) {
        progressList.add(CircleProgressDrawable(
            bgColor = backgroundColor,
            bgWidth = backgroundWidth,
            pColor = color,
            pWidth = width
        ).apply {
            this.progress = progress
            this.total = total
        })
    }

    /**
     * 清空进度
     */
    fun clearProgress() {
        progressList.clear()
    }

    /**
     * 刷新进度条
     */
    fun refresh() {
        invalidate()
    }

    init {
        // viewGroup不加这句话,就不会走onDraw
        setWillNotDraw(false)
    }

    /**
     * 收缩距离,onDraw()时,每绘制一条进度条,
     * scaleSize增加进度条宽度和间距宽度,
     * 用来确认下一个进度条绘制的大小
     */
    var scaleSize: Int = 0

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        scaleSize = 0
        progressList.forEach {
            // 设置 Drawable 边距
            it.setBounds(scaleSize, scaleSize, width - scaleSize, height - scaleSize)
            canvas?.let { canvas ->
                it.draw(canvas)
            }
            // 计算下一个进度条位置
            // it.bgWidth.coerceAtLeast(it.pWidth) 背景宽度和进度条宽度取最大值
            scaleSize += (it.bgWidth.coerceAtLeast(it.pWidth) + dividerWidth).toInt()
        }
    }

    private fun dp2px(dp: Float): Float {
        return (context.resources.displayMetrics.density * dp + 0.5f)
    }
}

这样,一个成熟的业务能用的控件就OK了,最后放一下页面演示代码

package com.elee.uidemo

import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

    private val mViewMcp : MultiCircleProgressView by lazy { findViewById(R.id.view_mcp) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

//        mViewMcp.addProgress(progress = 6, total = 12)
//        mViewMcp.addProgress(progress = 9, total = 12, color = Color.GREEN, backgroundColor = Color.parseColor("#1600FF00"))
        mViewMcp.addProgress(progress = 3, total = 12, color = Color.parseColor("#4077E6"), backgroundColor = Color.parseColor("#160000FF"))
        mViewMcp.addProgress(progress = 15, total = 12, color = Color.parseColor("#64D4C7"), backgroundColor = Color.parseColor("#160000FF"))
        mViewMcp.addProgress(progress = 18, total = 12, color = Color.parseColor("#F7963B"), backgroundColor = Color.parseColor("#160000FF"))
//        mViewMcp.addProgress(progress = 15, total = 12, color = Color.BLUE, backgroundColor = Color.parseColor("#160000FF"))
//        mViewMcp.addProgress(progress = 0, total = 12)
    }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#F8F9FA"
    tools:context=".MainActivity">

    <com.elee.uidemo.MultiCircleProgressView
        android:id="@+id/view_mcp"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

最后,来一道课后习题吧,弧形多进度进度条,一个弧形的背景上要展示多个不同进度~

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
总共分为三层:一层为圆形边线,一层为进度边线,一层用来显示标识进度节点。 public class CircleProgressBar extends View { private int maxProgress = 100; private int progress = 15; private int progressStrokeWidth = 2; private int marxArcStorkeWidth = 16; // 画圆所在的距形区域 RectF oval; Paint paint; public CircleProgressBar(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub oval = new RectF(); paint = new Paint(); } @Override protected void onDraw(Canvas canvas) { // TODO 自动生成的方法存根 super.onDraw(canvas); int width = this.getWidth(); int height = this.getHeight(); width = (width > height) ? height : width; height = (width > height) ? height : width; paint.setAntiAlias(true); // 设置画笔为抗锯齿 paint.setColor(Color.WHITE); // 设置画笔颜色 canvas.drawColor(Color.TRANSPARENT); // 白色背景 paint.setStrokeWidth(progressStrokeWidth); // 线宽 paint.setStyle(Style.STROKE); oval.left = marxArcStorkeWidth / 2; // 左上角x oval.top = marxArcStorkeWidth / 2; // 左上角y oval.right = width - marxArcStorkeWidth / 2; // 左下角x oval.bottom = height - marxArcStorkeWidth / 2; // 右下角y canvas.drawArc(oval, -90, 360, false, paint); // 绘制白色圆圈,即进度条背景 paint.setColor(Color.rgb(0x57, 0x87, 0xb6)); paint.setStrokeWidth(marxArcStorkeWidth); canvas.drawArc(oval, -90, ((float) progress / maxProgress) * 360, false, paint); // 绘制进度圆弧,这里是蓝色 paint.setStrokeWidth(1); String text = progress + "%"; int textHeight = height / 4; paint.setTextSize(textHeight); int textWidth = (int) paint.measureText(text, 0, text.length()); paint.setStyle(Style.FILL); canvas.drawText(text, width / 2 - textWidth / 2, height / 2 + textHeight / 2, paint); } public int getMaxProgress() { return maxProgress; } public void setMaxProgress(int maxProgress) { this.maxProgress = maxProgress; } /** * 设置进度 * * @param progress * 进度百分比 * @param view * 标识进度的节点视图 */ public void setProgress(int progress, View view) { this.progress = progress; view.setAnimation(pointRotationAnima(0, (int) (((float) 360 / maxProgress) * progress))); this.invalidate(); } /** * 非UI线程调用 */ public void setProgressNotInUiThread(int progress, View view) { this.progress = progress; view.setAnimation(pointRotationAnima(0, (int) (((float) 360 / maxProgress) * progress))); this.postInvalidate(); } /** * 进度标注点的动画 * * @param fromDegrees * @param toDegrees * @return */ private Animation pointRotationAnima(float fromDegrees, float toDegrees) { int initDegress = 306;// 进度点起始位置(图片偏移约54度) RotateAnimation animation = new RotateAnimation(fromDegrees, initDegress + toDegrees, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation.setDuration(1);// 设置动画执行时间 animation.setRepeatCount(1);// 设置重复执行次数 animation.setFillAfter(true);// 设置动画结束后是否停留在结束位置 return animation; } }
好的,下面是一个简单的圆形进度条自定义View实现: ```java public class CircleProgressBar extends View { private float mProgress = 0; // 当前进度值 private float mMax = 100; // 最大进度值 private int mCircleWidth = 10; // 圆环宽度 private int mCircleColor = Color.GRAY; // 圆环颜色 private int mProgressColor = Color.BLUE; // 进度条颜色 private Paint mPaint; public CircleProgressBar(Context context) { super(context); init(); } public CircleProgressBar(Context context, AttributeSet attrs) { super(context, attrs); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar); mCircleWidth = ta.getDimensionPixelSize(R.styleable.CircleProgressBar_circleWidth, 10); mCircleColor = ta.getColor(R.styleable.CircleProgressBar_circleColor, Color.GRAY); mProgressColor = ta.getColor(R.styleable.CircleProgressBar_progressColor, Color.BLUE); ta.recycle(); init(); } private void init() { mPaint = new Paint(); mPaint.setAntiAlias(true); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int centerX = getWidth() / 2; int centerY = getHeight() / 2; int radius = getWidth() / 2 - mCircleWidth / 2; // 画圆环 mPaint.setColor(mCircleColor); mPaint.setStrokeWidth(mCircleWidth); mPaint.setStyle(Paint.Style.STROKE); canvas.drawCircle(centerX, centerY, radius, mPaint); // 画进度条 mPaint.setColor(mProgressColor); mPaint.setStrokeWidth(mCircleWidth); mPaint.setStyle(Paint.Style.STROKE); RectF rectF = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius); canvas.drawArc(rectF, -90, 360 * mProgress / mMax, false, mPaint); } public void setProgress(float progress) { mProgress = progress; invalidate(); } public void setMax(float max) { mMax = max; invalidate(); } } ``` 其中,我们可以设置圆环的宽度、圆环颜色、进度条颜色等属性。在onDraw()方法中,我们先画出圆环,然后再画出进度条进度条的弧度根据当前进度值和最大进度值计算得出。 使用时,可以在布局文件中添加如下代码: ```xml <com.example.customview.CircleProgressBar android:id="@+id/circle_progress_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" app:circleColor="#FFA500" app:circleWidth="20dp" app:progressColor="#00BFFF" /> ``` 然后在代码中设置进度值即可: ```java CircleProgressBar circleProgressBar = findViewById(R.id.circle_progress_bar); circleProgressBar.setMax(100); circleProgressBar.setProgress(50); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值