简单实现圆角矩形进度条

758 阅读3分钟

在这个博客中,我们将探讨如何使用 Android 的 PathMeasure 类来创建一个带圆角的矩形进度条。通过一个具体的示例——CountingProgress 类,来展示如何有效地利用 PathMeasure 和 Paint 类来实现这一效果。

概述

PathMeasure 是一个强大的工具,它可以用来测量 Path 的长度和提取路径的一部分。这在制作自定义形状的进度条时非常有用。在本例中,我们将实现一个圆角矩形进度条,它的颜色从起始颜色渐变到结束颜色。

CountingProgress 类详解

我们的 CountingProgress 类继承自 View,并实现了以下功能:

  1. 构造函数:我们提供了三个构造函数以支持从 XML 布局文件中使用此视图。

  2. 属性定义

    • currentProgress: 表示当前进度的变量。
    • padding: 绘制区域的内边距。
    • startColor 和 endColor: 进度条的起始和结束颜色。
    • cornerRadius: 矩形角的圆角半径。
    • curProgressWidth: 进度条的宽度。
    • pathPaint: 用于绘制进度条的 Paint 对象,配置为使用线性渐变和反锯齿。
  3. 设置进度

    • setProgress(int): 允许外部调用此方法来设置进度,并触发视图的重绘。
  4. 绘制逻辑

    • 在 onDraw 方法中,我们首先定义了进度条的绘制区域。
    • 使用 Path 类定义了一个圆角矩形路径。
    • PathMeasure 被用来测量该路径,并根据当前进度计算应该绘制的路径长度。
    • 使用 getSegment 方法从完整路径中截取相应长度的路径部分。
    • 最后,使用配置好的 pathPaint 将截取的路径绘制到画布上。

代码实现

class CountingProgress : View {

    // 当前进度
    private var currentProgress = 0

    // 边距
    private val padding = dp2px(1.5f).toFloat()

    // 渐变开始颜色
    private val startColor = Color.parseColor("#FFC305")

    // 渐变结束颜色
    private val endColor = Color.parseColor("#80EE0B")

    // 当前矩形原角度
    private var cornerRadius = dp2px(10f).toFloat()

    // 当前进度条画笔的宽度
    private val curProgressWidth: Float by lazy {
        dp2px(3f).toFloat()
    }
    
    // 是否居中
    private var isCentered = true

    // 当前进度条画笔
    private val pathPaint: Paint by lazy {
        Paint().apply {
            isAntiAlias = true
            style = Paint.Style.STROKE
            strokeWidth = curProgressWidth
            shader = LinearGradient(
                0f, 0f, width.toFloat(), 0f, startColor, endColor, Shader.TileMode.CLAMP
            )
        }
    }

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context, attrs, defStyleAttr
    )

    // 设置进度
    fun setProgress(progress: Int) {
        currentProgress = progress
        invalidate()
    }

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 测量路径
        val measure = PathMeasure(getMeasurePath(), true)
        // 创建目标路径
        val dst = Path()
        // 计算进度段
        val progressLength = measure.length * currentProgress / 100f
        // 获取截取路径
        if (progressLength > 0) {
            measure.getSegment(0f, progressLength, dst, true)
        }
        // 绘制路径
        canvas.drawPath(dst, pathPaint)
    }

    /**
     * 获取测量路径
     * @return Path
     */
    private fun getMeasurePath(): Path {
        // 矩形大小
        val maxRect = RectF(padding, padding, width - padding, height - padding)
        // 创建路径和测量工具
        val path = Path()
        // 是否从顶部居中开始
        if (isCentered) {
            // 从顶部中间开始绘制路径
            val topMiddleX = maxRect.left + maxRect.width() / 2
            path.moveTo(topMiddleX, maxRect.top)
            
            // 绘制到右上角并圆角化
            path.arcTo(RectF(maxRect.right - cornerRadius * 2, maxRect.top, maxRect.right, maxRect.top + cornerRadius * 2), -90f, 90f)
            // 绘制右边
            path.lineTo(maxRect.right, maxRect.bottom - cornerRadius)
            // 绘制右下角圆角
            path.arcTo(RectF(maxRect.right - cornerRadius * 2, maxRect.bottom - cornerRadius * 2, maxRect.right, maxRect.bottom), 0f, 90f)
            // 绘制底部
            path.lineTo(maxRect.left + cornerRadius, maxRect.bottom)
            // 绘制左下角圆角
            path.arcTo(RectF(maxRect.left, maxRect.bottom - cornerRadius * 2, maxRect.left + cornerRadius * 2, maxRect.bottom), 90f, 90f)
            // 绘制左边
            path.lineTo(maxRect.left, maxRect.top + cornerRadius)
            // 绘制左上角圆角
            path.arcTo(RectF(maxRect.left, maxRect.top, maxRect.left + cornerRadius * 2, maxRect.top + cornerRadius * 2), 180f, 90f)

            // 关闭路径形成闭环
            path.close()
        } else {
            path.addRoundRect(maxRect, cornerRadius, cornerRadius, Path.Direction.CW)
        }
        return path
      }
    }

在XML中如何使用

<RelativeLayout
    android:id="@+id/rlCountView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginHorizontal="16dp"
    android:layout_marginTop="30dp">
    <TextView
        android:id="@+id/tvCount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:background="@drawable/btn_12_black_bg"
        android:gravity="center"
        android:minWidth="90dp"
        android:paddingHorizontal="7dp"
        android:text="0"
        android:textColor="@color/white"
        android:textSize="41sp"
        android:textStyle="bold" />

    <com.example.modjumprope.widgets.CountingProgress
        android:id="@+id/countingProgress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignStart="@+id/tvCount"
        android:layout_alignTop="@+id/tvCount"
        android:layout_alignEnd="@+id/tvCount"
        android:layout_alignBottom="@+id/tvCount"/>
</RelativeLayout>

示例图

Screenshot_20240625_181622.png

小结

通过使用 PathMeasure,我们能够精确控制路径的绘制,这对于创建复杂形状的进度条非常有用。这种方法不仅提高了 UI 的灵活性,还能够提供平滑和吸引人的视觉效果。希望这篇博客能够帮助你在自己的项目中实现更多自定义的视觉元素。