android 封段分段进度条自定义View

16 阅读6分钟

image.png

package com.steelmate.company.views;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;

import com.steelmate.company.R;

/**
 * 分段进度条自定义View
 * <p>
 * 支持将进度条分为多个段(segment),每段之间留有间隔,
 * 进度颜色从起始色到结束色渐变过渡,首尾段支持圆角绘制。
 * 可通过XML属性或Java代码配置段数、颜色、圆角、间距、边框等样式。
 * </p>
 */
public class SegmentedProgressBar extends View {

    /** 背景层画笔:用于绘制进度条整体背景 */
    private Paint backgroundPaint;
    /** 内容背景层画笔:用于绘制边框内部、进度条下方的白色内容区域背景 */
    private Paint contentBackgroundPaint;
    /** 进度条画笔:用于绘制每一段已填充的进度色块 */
    private Paint progressPaint;
    /** 边框画笔:用于绘制进度条外边框描边 */
    private Paint borderPaint;
    /** 间隔线画笔:用于绘制各分段之间的间隔填充(白色间隔) */
    private Paint spacingPaint;

    /** 最大分段数,默认5段 */
    private int maxSegments = 5;
    /** 当前进度值,范围0~100 */
    private int currentProgress = 0;

    /** 进度条整体圆角半径(像素) */
    private float cornerRadius = 8f;
    /** 分段之间的间隔宽度(像素) */
    private float segmentSpacing = 1f;
    /** 外边框描边宽度(像素) */
    private float borderWidth = 2f;
    /** 边框与内容区域之间的内边距(像素) */
    private float contentPadding = 0f;

    /** 进度条背景颜色,默认灰色 #D0D0D0 */
    private int backgroundColor = Color.parseColor("#D0D0D0");
    /** 内容区域背景颜色(边框内侧),默认白色 */
    private int contentBackgroundColor = Color.WHITE;
    /** 进度条起始颜色(低进度端),默认绿色 #4CAF50 */
    private int progressStartColor = Color.parseColor("#4CAF50");
    /** 进度条结束颜色(高进度端),默认浅绿色 #8BC34A */
    private int progressEndColor = Color.parseColor("#8BC34A");
    /** 外边框颜色,默认白色 */
    private int borderColor = Color.WHITE;

    /** 进度条整体背景矩形区域 */
    private RectF backgroundRect;
    /** 内容区域矩形(边框内侧) */
    private RectF contentRect;
    /** 当前绘制段进度矩形区域(复用对象,避免频繁创建) */
    private RectF progressRect;
    /** 进度段路径,用于实现选择性圆角绘制 */
    private Path progressPath;

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

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

    public SegmentedProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    /**
     * 初始化画笔、矩形区域及路径对象,并从XML属性集中读取自定义属性值
     *
     * @param context 上下文
     * @param attrs   XML属性集,可为null
     */
    private void init(Context context, AttributeSet attrs) {
        if (attrs != null) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SegmentedProgressBar);
            maxSegments = ta.getInt(R.styleable.SegmentedProgressBar_maxSegments, 5);
            cornerRadius = ta.getDimension(R.styleable.SegmentedProgressBar_cornerRadius, 8f);
            segmentSpacing = ta.getDimension(R.styleable.SegmentedProgressBar_segmentSpacing, 1f);
            borderWidth = ta.getDimension(R.styleable.SegmentedProgressBar_borderWidth, 2f);
            contentPadding = ta.getDimension(R.styleable.SegmentedProgressBar_contentPadding, 0f);
            backgroundColor = ta.getColor(R.styleable.SegmentedProgressBar_backgroundColor, Color.parseColor("#D0D0D0"));
            contentBackgroundColor = ta.getColor(R.styleable.SegmentedProgressBar_contentBackgroundColor, Color.WHITE);
            progressStartColor = ta.getColor(R.styleable.SegmentedProgressBar_progressStartColor, Color.parseColor("#4CAF50"));
            progressEndColor = ta.getColor(R.styleable.SegmentedProgressBar_progressEndColor, Color.parseColor("#8BC34A"));
            borderColor = ta.getColor(R.styleable.SegmentedProgressBar_borderColor, Color.WHITE);
            ta.recycle();
        }

        backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        backgroundPaint.setColor(backgroundColor);
        backgroundPaint.setStyle(Paint.Style.FILL);

        contentBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        contentBackgroundPaint.setColor(contentBackgroundColor);
        contentBackgroundPaint.setStyle(Paint.Style.FILL);

        progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        progressPaint.setStyle(Paint.Style.FILL);

        borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        borderPaint.setColor(borderColor);
        borderPaint.setStyle(Paint.Style.STROKE);
        borderPaint.setStrokeWidth(borderWidth);

        spacingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        spacingPaint.setColor(Color.WHITE);
        spacingPaint.setStyle(Paint.Style.FILL);

        backgroundRect = new RectF();
        contentRect = new RectF();
        progressRect = new RectF();
        progressPath = new Path();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        // 当高度模式为AT_MOST(如wrap_content)时,根据圆角、边框、内边距计算默认高度
        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
            height = (int) (cornerRadius * 2 + borderWidth * 2 + contentPadding * 2 + 10);
        }

        setMeasuredDimension(width, height);
    }

    /**
     * 绘制进度条,绘制顺序:
     * 1. 背景圆角矩形
     * 2. 外边框描边
     * 3. 内容区域背景(白色底)
     * 4. 已填充的进度段(带选择性圆角)
     * 5. 部分填充的进度段
     * 6. 分段间隔线
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        float paddingLeft = getPaddingLeft();
        float paddingRight = getPaddingRight();
        float paddingTop = getPaddingTop();
        float paddingBottom = getPaddingBottom();

        // 1. 绘制背景圆角矩形
        backgroundRect.set(paddingLeft, paddingTop,
                getWidth() - paddingRight,
                getHeight() - paddingBottom);

        backgroundPaint.setColor(backgroundColor);
        canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint);

        // 2. 绘制外边框
        canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, borderPaint);

        // 3. 计算并绘制内容区域背景(边框内侧)
        float innerLeft = paddingLeft + borderWidth + contentPadding;
        float innerTop = paddingTop + borderWidth + contentPadding;
        float innerRight = getWidth() - paddingRight - borderWidth - contentPadding;
        float innerBottom = getHeight() - paddingBottom - borderWidth - contentPadding;

        contentRect.set(innerLeft, innerTop, innerRight, innerBottom);

        contentBackgroundPaint.setColor(contentBackgroundColor);
        canvas.drawRoundRect(contentRect, cornerRadius / 2, cornerRadius / 2, contentBackgroundPaint);

        if (currentProgress > 0) {
            // 计算每段宽度和已填满的段数
            float totalSpacing = segmentSpacing * (maxSegments - 1);
            float segmentWidth = (contentRect.width() - totalSpacing) / maxSegments;

            int filledSegments = (int) ((float) currentProgress / 100f * maxSegments);
            float segmentProgress = ((float) currentProgress / 100f * maxSegments) - filledSegments;

            // 4. 绘制已完全填充的进度段
            for (int i = 0; i < filledSegments; i++) {
                float left = innerLeft + i * (segmentWidth + segmentSpacing);
                float right = left + segmentWidth;

                progressRect.set(left, innerTop, right, innerBottom);

                // 根据当前段位置计算颜色渐变比例
                float progressRatio = (float) (i + 1) / maxSegments;
                int interpolatedColor = interpolateColor(progressStartColor, progressEndColor, progressRatio);
                progressPaint.setColor(interpolatedColor);

                // 首段左侧圆角,末段右侧圆角
                boolean isFirstSegment = (i == 0);
                boolean isLastSegment = (i == maxSegments - 1);

                drawSegmentWithCorners(canvas, progressRect, cornerRadius / 2,
                        isFirstSegment, isLastSegment, isFirstSegment, isLastSegment);
            }

            // 5. 绘制部分填充的进度段(非整段进度)
            if (filledSegments < maxSegments && segmentProgress > 0) {
                float left = innerLeft + filledSegments * (segmentWidth + segmentSpacing);
                float partialWidth = segmentWidth * segmentProgress;
                float right = left + partialWidth;

                if (right > left) {
                    progressRect.set(left, innerTop, right, innerBottom);

                    float progressRatio = (float) (filledSegments + segmentProgress) / maxSegments;
                    int interpolatedColor = interpolateColor(progressStartColor, progressEndColor, progressRatio);
                    progressPaint.setColor(interpolatedColor);

                    boolean isFirstSegment = (filledSegments == 0);
                    boolean isLastSegment = (filledSegments == maxSegments - 1);

                    drawSegmentWithCorners(canvas, progressRect, cornerRadius / 2,
                            isFirstSegment, isLastSegment, isFirstSegment, isLastSegment);
                }
            }

            // 6. 绘制分段间隔线
            drawSpacingLines(canvas, innerLeft, innerTop, innerRight, innerBottom, segmentWidth);
        }
    }

    /**
     * 使用Path绘制带选择性圆角的进度段
     * <p>
     * 通过为矩形的四个角分别设置圆角半径(0或有值),实现首段左圆角、末段右圆角等效果。
     * </p>
     *
     * @param canvas      画布
     * @param rect        进度段矩形区域
     * @param radius      圆角半径
     * @param topLeft     是否绘制左上角圆角
     * @param topRight    是否绘制右上角圆角
     * @param bottomLeft  是否绘制左下角圆角
     * @param bottomRight 是否绘制右下角圆角
     */
    private void drawSegmentWithCorners(Canvas canvas, RectF rect, float radius,
                                        boolean topLeft, boolean topRight,
                                        boolean bottomLeft, boolean bottomRight) {
        progressPath.reset();

        // 构建8个值的圆角半径数组:每个角对应2个值(x半径, y半径)
        float[] radii = new float[8];
        radii[0] = topLeft ? radius : 0;
        radii[1] = topLeft ? radius : 0;
        radii[2] = topRight ? radius : 0;
        radii[3] = topRight ? radius : 0;
        radii[4] = bottomRight ? radius : 0;
        radii[5] = bottomRight ? radius : 0;
        radii[6] = bottomLeft ? radius : 0;
        radii[7] = bottomLeft ? radius : 0;

        progressPath.addRoundRect(rect, radii, Path.Direction.CW);
        canvas.drawPath(progressPath, progressPaint);
    }

    /**
     * 绘制分段之间的间隔线
     * <p>
     * 在每两个相邻段之间绘制白色矩形填充,视觉上形成分段间隔效果。
     * </p>
     *
     * @param canvas      画布
     * @param left        内容区域左边界
     * @param top         内容区域上边界
     * @param right       内容区域右边界
     * @param bottom      内容区域下边界
     * @param segmentWidth 单个段的宽度
     */
    private void drawSpacingLines(Canvas canvas, float left, float top, float right, float bottom, float segmentWidth) {
        for (int i = 1; i < maxSegments; i++) {
            float spacingX = left + i * (segmentWidth + segmentSpacing) - segmentSpacing;
            RectF spacingRect = new RectF(spacingX, top, spacingX + segmentSpacing, bottom);
            canvas.drawRect(spacingRect, spacingPaint);
        }
    }

    /**
     * 在两个颜色之间进行线性插值
     *
     * @param color1   起始颜色
     * @param color2   结束颜色
     * @param fraction 插值比例,0.0~1.0
     * @return 插值后的ARGB颜色值
     */
    private int interpolateColor(int color1, int color2, float fraction) {
        int a = (int) (Color.alpha(color1) + fraction * (Color.alpha(color2) - Color.alpha(color1)));
        int r = (int) (Color.red(color1) + fraction * (Color.red(color2) - Color.red(color1)));
        int g = (int) (Color.green(color1) + fraction * (Color.green(color2) - Color.green(color1)));
        int b = (int) (Color.blue(color1) + fraction * (Color.blue(color2) - Color.blue(color1)));
        return Color.argb(a, r, g, b);
    }

    /**
     * 设置当前进度值
     *
     * @param progress 进度值,范围0~100,超出范围会自动裁剪
     */
    public void setProgress(int progress) {
        this.currentProgress = Math.min(100, Math.max(0, progress));
        invalidate();
    }

    /**
     * 获取当前进度值
     *
     * @return 当前进度,范围0~100
     */
    public int getProgress() {
        return currentProgress;
    }

    /**
     * 设置最大分段数
     *
     * @param segments 分段数量
     */
    public void setMaxSegments(int segments) {
        this.maxSegments = segments;
        invalidate();
    }

    /**
     * 设置进度条渐变的起始和结束颜色
     *
     * @param startColor 起始颜色(低进度端)
     * @param endColor   结束颜色(高进度端)
     */
    public void setColors(int startColor, int endColor) {
        this.progressStartColor = startColor;
        this.progressEndColor = endColor;
        invalidate();
    }
}
```
```

```
res 下 color 文件新增如下颜色值
```
<!-- ... existing code ... -->
<color name="battery_progress_background">#E0E0E0</color>
<color name="battery_progress_start">#4CAF50</color>
<color name="battery_progress_end">#8BC34A</color>
<color name="battery_low_color">#FF5722</color>
<color name="battery_medium_color">#FFC107</color>
<color name="battery_high_color">#4CAF50</color>
<!-- ... existing code ... -->
```
```

```
attr.xml 文件新增自定义样式属性
```
 <!-- ... existing code ... -->
    <declare-styleable name="SegmentedProgressBar">
        <attr name="maxSegments" format="integer"/>
        <attr name="cornerRadius" format="dimension"/>
        <attr name="segmentSpacing" format="dimension"/>
        <attr name="padding" format="dimension"/>
        <attr name="contentPadding" format="dimension"/>
        <attr name="borderWidth" format="dimension"/>
        <attr name="backgroundColor" format="color"/>
        <attr name="contentBackgroundColor" format="color"/>
        <attr name="progressStartColor" format="color"/>
        <attr name="progressEndColor" format="color"/>
        <attr name="borderColor" format="color"/>
        <attr name="dividerColor" format="color"/>
    </declare-styleable>
<!-- ... existing code ... -->
```
```

```
layout 文件下新增自定义SegmentedProgressBar 文件
```
<com.steelmate.company.views.SegmentedProgressBar
    android:id="@+id/progressBar"
    android:layout_width="224dp"
    android:layout_height="14dp"
    android:layout_marginTop="8dp"
    android:max="100"
    android:progress="55"
    app:maxSegments="5"
    app:cornerRadius="8dp"
    app:segmentSpacing="1dp"
    app:borderWidth="4dp"
    app:contentPadding="-2dp"
    app:backgroundColor="#D0D0D0"
    app:contentBackgroundColor="@color/transparent"
    app:progressStartColor="#4CAF50"
    app:progressEndColor="#8BC34A"
    app:borderColor="#FFFFFF"
    app:dividerColor="#FFFFFF"
    app:layout_constraintStart_toStartOf="@id/ivBatteryIcon"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toBottomOf="@id/tvRemainingCapacity" />
```
```
  private fun setupSegmentedProgress() {
//        val progressBar = findViewById<SegmentedProgressBar>(R.id.progressBar)
//        val tvPercentage = findViewById<TextView>(R.id.tvPercentage)
//        val tvEstimatedRange = findViewById<TextView>(R.id.tvEstimatedRange)


        val batteryLevel = 45
        val estimatedRange = 42

        contentView.progressBar.progress = batteryLevel
        contentView.tvPercentage.text = "$batteryLevel%"
        contentView.tvEstimatedRange.text = "预估里程 ${estimatedRange}km"

        when {
            batteryLevel <= 20 -> {
                contentView.progressBar.setColors(Color.parseColor("#FF5722"), Color.parseColor("#FF5722"))
        }
            batteryLevel <= 50 -> {
                contentView.progressBar.setColors(Color.parseColor("#FFC107"), Color.parseColor("#FFC107"))
        }
            else -> {
                contentView.progressBar.setColors(Color.parseColor("#4CAF50"), Color.parseColor("#8BC34A"))
            }
        }
    }
```
```