类似表盘的环形进度条_Math

package com.test.demo1.weight;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;

import com.test.demo1.R;

//https://cloud.tencent.com/developer/article/2271395
//https://hurui1990.github.io/hurui.github.io/2017/08/22/Android%E8%87%AA%E5%AE%9A%E4%B9%89View%E4%B9%8B%E8%87%AA%E5%B7%B1%E7%94%BB%E4%B8%80%E4%B8%AA%E6%97%B6%E9%92%9F/
//https://cloud.tencent.com/developer/article/1918397
//https://github.com/Blankj/ProgressRing
public class CircularProgressBar extends View {

    /**
     * 半径
     */
    private int mRadius;
    /**
     * 进度条宽度
     */
    private int mStrokeWidth;

    /**
     * 进度条背景颜色
     */
    private int mProgressbarBgColor;

    /**
     * 进度条进度颜色
     */
    private int mProgressColor;

    /**
     * 开始角度
     */
    private int mStartAngle = 0;

    /**
     * 当前角度
     */
    private float mCurrentAngle = 0;

    /**
     * 结束角度
     */
    private int mEndAngle = 360;
    /**
     * 最大进度
     */
    private float mMaxProgress;
    /**
     * 当前进度
     */
    private float mCurrentProgress;
    /**
     * 上次进度
     */
    private float mLastProgress;
    /**
     * 文字
     */
    private String mText;
    /**
     * 文字颜色
     */
    private int mTextColor;
    /**
     * 文字大小
     */
    private float mTextSize;
    /**
     * 动画的执行时长
     */
    private long mDuration = 1000;
    /**
     * 是否执行动画
     */
    private boolean isAnimation = false;
    /**
     * 等级
     */
    private Level level;

    public CircularProgressBar(Context context) {
        this(context, null);
    }

    public CircularProgressBar(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircularProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircularProgressBar);
        mRadius = array.getDimensionPixelSize(R.styleable.CircularProgressBar_radius, 80);
        mStrokeWidth = array.getDimensionPixelSize(R.styleable.CircularProgressBar_strokeWidth, 8);
        mProgressbarBgColor = array.getColor(R.styleable.CircularProgressBar_progressbarBackgroundColor, ContextCompat.getColor(context, R.color.teal_700));
        mProgressColor = array.getColor(R.styleable.CircularProgressBar_progressbarColor, ContextCompat.getColor(context, R.color.teal_200));
        mMaxProgress = array.getInt(R.styleable.CircularProgressBar_maxProgress, 100);
        mCurrentProgress = array.getInt(R.styleable.CircularProgressBar_progress, 0);
        String text = array.getString(R.styleable.CircularProgressBar_text);
        mText = text == null ? "" : text;
        mTextColor = array.getColor(R.styleable.CircularProgressBar_textColor, ContextCompat.getColor(context, R.color.black));
        mTextSize = array.getDimensionPixelSize(R.styleable.CircularProgressBar_textSize, (int) TypedValue
                .applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));
        array.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = 0;
        switch (MeasureSpec.getMode(widthMeasureSpec)) {
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.AT_MOST:   //wrap_content
                width = mRadius * 2;
                break;
            case MeasureSpec.EXACTLY:   //match_parent
                width = MeasureSpec.getSize(widthMeasureSpec);
                break;
        }
        //Set the measured width and height
        setMeasuredDimension(width, width);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int centerX = getWidth() / 2;
        RectF rectF = new RectF();
        rectF.left = mStrokeWidth;
        rectF.top = mStrokeWidth;
        rectF.right = centerX * 2 - mStrokeWidth;
        rectF.bottom = centerX * 2 - mStrokeWidth;

        if (level == Level.LV3) {
            mTextColor = mProgressColor = Color.parseColor("#3EE0CE");
            mProgressbarBgColor = Color.parseColor("#8CE7DF");
        } else if (level == Level.LV2) {
            mTextColor = mProgressColor = Color.parseColor("#E08540");
            mProgressbarBgColor = Color.parseColor("#E7B48E");
        } else {
            mTextColor = mProgressColor = Color.parseColor("#E13F3F");
            mProgressbarBgColor = Color.parseColor("#E68D8F");

        }

        // 绘制内侧红色圆环
        drawInnerCircle(canvas, centerX);

        //绘制进度条背景
        drawProgressbarBg(canvas, rectF);
//        //绘制进度
        drawProgress(canvas, rectF);
        //绘制中心文本
        drawCenterText(canvas, centerX);
        //绘制刻度
        drawScale(canvas, centerX);
    }

    // 新增成员变量
    private int mInnerCircleStrokeWidth = 10; // 内侧圆环宽度,默认10像素
    private int mInnerCircleSpacing = 5;     // 与进度条背景的间距,默认20像素
    private int mInnerCircleColor = Color.RED; // 内侧圆环的颜色

    /**
     * 绘制内侧红色圆环
     */
    private void drawInnerCircle(Canvas canvas, int centerX) {
        Paint mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mInnerCircleStrokeWidth);
        mPaint.setAntiAlias(true);
        mPaint.setColor(mProgressbarBgColor);
        mPaint.setStrokeCap(Paint.Cap.ROUND);

        // 定义内侧圆环的绘制区域,考虑到间隔
        float innerRadius = centerX - mInnerCircleSpacing - mInnerCircleStrokeWidth / 10;
//        float innerRadius = 2;
        RectF innerCircleRectF = new RectF(
                centerX - innerRadius,
                centerX - innerRadius,
                centerX + innerRadius,
                centerX + innerRadius);

        // 绘制内侧红色圆环
        canvas.drawArc(innerCircleRectF, 0, 360, false, mPaint);
    }

    private void drawScale(Canvas canvas, int centerX) {
        Paint mPaintShort = new Paint(Paint.ANTI_ALIAS_FLAG); // 创建了一个画笔(Paint),并进行一些属性的设置
        Paint mPaintLong = new Paint(Paint.ANTI_ALIAS_FLAG); // 创建了一个画笔(Paint),并进行一些属性的设置
        mPaintLong.setColor(mProgressbarBgColor);
        mPaintShort.setColor(mProgressColor);

        // 计算内圆半径(刻度线开始位置)和外圆半径(刻度线结束位置)
        float innerRadius = centerX - mStrokeWidth * 2; // 内圆半径,即进度条内侧边界
        float outerRadiusForText = centerX - mTextSize - mStrokeWidth; // 文字与刻度线之间的间距

        for (int i = 0; i < 60; i++) {
            if (i % 5 == 0) { // 显示整点数的较长刻度线
                mPaintLong.setStrokeWidth(mStrokeWidth / 8); // 设置一个合适的宽度
                // 长刻度线从内圆边缘开始绘制,向外延伸至接近文字区域
                canvas.drawLine(
                        (float) (centerX + innerRadius * Math.cos(Math.toRadians(i * 6))), (float) (centerX + innerRadius * Math.sin(Math.toRadians(i * 6))),
                        (float) (centerX + outerRadiusForText * Math.cos(Math.toRadians(i * 6))), (float) (centerX + outerRadiusForText * Math.sin(Math.toRadians(i * 6))
                        ), mPaintLong);
            } else {
                mPaintShort.setStrokeWidth(mStrokeWidth / 16); // 设置一个合适的宽度
                // 短刻度线从内圆边缘开始绘制,向外延伸至接近文字区域
                canvas.drawLine(
                        (float) (centerX + innerRadius * Math.cos(Math.toRadians(i * 6))), (float) (centerX + innerRadius * Math.sin(Math.toRadians(i * 6))),
                        (float) (centerX + (outerRadiusForText + mStrokeWidth / 4) * Math.cos(Math.toRadians(i * 6))), (float) (centerX + (outerRadiusForText + mStrokeWidth / 4) * Math.sin(Math.toRadians(i * 6))
                        ), mPaintShort);
            }
        }
    }

    /**
     * 绘制进度条背景
     */
    private void drawProgressbarBg(Canvas canvas, RectF rectF) {
        Paint mPaint = new Paint();
        //画笔的填充样式,Paint.Style.STROKE 描边
        mPaint.setStyle(Paint.Style.STROKE);
        //圆弧的宽度
        mPaint.setStrokeWidth(mStrokeWidth);
        //抗锯齿
        mPaint.setAntiAlias(true);
        //画笔的颜色
        mPaint.setColor(mProgressbarBgColor);
        //画笔的样式 Paint.Cap.Round 圆形
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        //开始画圆弧
        canvas.drawArc(rectF, mStartAngle, mEndAngle, false, mPaint);
    }

    /**
     * 绘制进度
     */
    private void drawProgress(Canvas canvas, RectF rectF) {
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(mStrokeWidth);
        paint.setColor(mProgressColor);
        paint.setAntiAlias(true);
        paint.setStrokeCap(Paint.Cap.ROUND);
        if (!isAnimation) {
            mCurrentAngle = 360 * (mCurrentProgress / mMaxProgress);
        }
        mLastProgress = mCurrentAngle;
        canvas.drawArc(rectF, mStartAngle, mCurrentAngle, false, paint);
    }

    /**
     * 绘制中心文字
     */
    private void drawCenterText(Canvas canvas, int centerX) {
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(mTextColor);
        paint.setTextAlign(Paint.Align.CENTER);
        paint.setTextSize(mTextSize);
        Rect textBounds = new Rect();
        paint.getTextBounds(mText, 0, mText.length(), textBounds);
        canvas.drawText(mText, centerX, textBounds.height() / 2 + getHeight() / 2, paint);
    }

    /**
     * 设置当前进度
     */
    public void setProgress(float progress, Level level) {
        this.level = level;
        if (progress < 0) {
            throw new IllegalArgumentException("Progress value can not be less than 0");
        }
        if (progress > mMaxProgress) {
            progress = mMaxProgress;
        }
        mCurrentProgress = progress;
        mCurrentAngle = 360 * (mCurrentProgress / mMaxProgress);
        if (mLastProgress == 0 || mLastProgress == mCurrentAngle) {
            setAnimator(0, mCurrentAngle);
        } else {
            setAnimator(mLastProgress, mCurrentAngle);
        }
    }

    /**
     * 设置文本
     */
    public void setText(String text) {
        mText = text;
    }

    /**
     * 设置文本的颜色
     */
    public void setTextColor(int color) {
        if (color <= 0) {
            throw new IllegalArgumentException("Color value can not be less than 0");
        }
        mTextColor = color;
    }

    /**
     * 设置文本的大小
     */
    public void setTextSize(float textSize) {
        if (textSize <= 0) {
            throw new IllegalArgumentException("textSize can not be less than 0");
        }
        mTextSize = textSize;
    }


    /**
     * 设置动画
     *
     * @param start  开始位置
     * @param target 结束位置
     */
    private void setAnimator(float start, float target) {
        isAnimation = true;
        ValueAnimator animator = ValueAnimator.ofFloat(start, target);
        animator.setDuration(mDuration);
        animator.setTarget(mCurrentAngle);//设置动画目标
        //动画更新监听
        animator.addUpdateListener(valueAnimator -> {
            mCurrentAngle = (float) valueAnimator.getAnimatedValue();
            invalidate();
        });
        animator.start();
    }

    public enum Level {
        LV1, LV2, LV3
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:gravity="center"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">



<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <Button
        android:id="@+id/btn_80"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="80" />
    <Button
        android:id="@+id/btn_60"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="60" />
    <Button
        android:id="@+id/btn_30"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="30" />
</LinearLayout>

    <com.test.demo1.weight.CircularProgressBar
        android:id="@+id/cpb_test"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        app:maxProgress="100"
        app:progress="10"
        app:progressbarBackgroundColor="@color/purple_500"
        app:progressbarColor="@color/purple_200"
        app:radius="80dp"
        app:strokeWidth="16dp"
        app:text="10"
        app:textColor="@color/teal_200"
        app:textSize="28sp" />

    <Button
        android:id="@+id/btn_set_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="随机设置进度" />
</LinearLayout>

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--文字颜色-->
    <attr name="textColor" format="color|reference" />
    <!--文字大小-->
    <attr name="textSize" format="dimension" />
    <!--蓝牙地址输入控件-->
    <declare-styleable name="MacAddressEditText">
        <!-- 方框大小,宽高一致 -->
        <attr name="boxWidth" format="dimension" />
        <!-- 方框背景颜色 -->
        <attr name="boxBackgroundColor" format="color|reference" />
        <!-- 方框描边颜色 -->
        <attr name="boxStrokeColor" format="color|reference" />
        <!-- 方框描边宽度 -->
        <attr name="boxStrokeWidth" format="dimension" />
        <!--文字颜色-->
        <attr name="textColor" />
        <!--文字大小-->
        <attr name="textSize" />
        <!--分隔符,: 、- -->
        <attr name="separator" format="string|reference" />
    </declare-styleable>
    <!--圆形进度条控件-->
    <declare-styleable name="CircularProgressBar">
        <!--半径-->
        <attr name="radius" format="dimension" />
        <!--进度条宽度-->
        <attr name="strokeWidth" format="dimension" />
        <!--进度条背景颜色-->
        <attr name="progressbarBackgroundColor" format="color|reference" />
        <!--进度条进度颜色-->
        <attr name="progressbarColor" format="color|reference" />
        <!--最大进度-->
        <attr name="maxProgress" format="integer" />
        <!--当前进度-->
        <attr name="progress" format="integer" />
        <!--文字-->
        <attr name="text" format="string" />
        <!--文字颜色-->
        <attr name="textColor" />
        <!--文字大小-->
        <attr name="textSize" />
    </declare-styleable>
</resources>
import android.os.Bundle;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.test.demo1.weight.CircularProgressBar;

import java.util.Random;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        //圆形进度条操作
        CircularProgressBar cpbTest = findViewById(R.id.cpb_test);
        Button btnSetProgress = findViewById(R.id.btn_set_progress);
        btnSetProgress.setOnClickListener(v -> {
            int progress = Math.abs(new Random().nextInt() % 100);
            Toast.makeText(this, "" + progress, Toast.LENGTH_SHORT).show();
            cpbTest.setText(progress + "");
            cpbTest.setProgress(progress, CircularProgressBar.Level.LV1);
        });
        findViewById(R.id.btn_80).setOnClickListener(v -> {
            cpbTest.setText(80 + "");
            cpbTest.setProgress(80, CircularProgressBar.Level.LV1);
        });
        findViewById(R.id.btn_60).setOnClickListener(v -> {
            cpbTest.setText(60 + "");
            cpbTest.setProgress(60, CircularProgressBar.Level.LV2);
        });
        findViewById(R.id.btn_30).setOnClickListener(v -> {
            cpbTest.setText(30 + "");
            cpbTest.setProgress(30, CircularProgressBar.Level.LV3);
        });
    }
}