You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使自定义View的进度边框从左上角而非左下角开始绘制?

问题:修改自定义圆角矩形进度边框的起始绘制位置

当前拖动滑块时,自定义圆角矩形边框从左下角开始绘制,需要修改为从左上角(或任意指定位置)开始。

原自定义View代码

internal class V(context: Context?) : View(context) {
    var path: Path = Path()
    var paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
    var length: Float = 0f
    var intervals: FloatArray = floatArrayOf(0f, 0f)

    init {
        paint.color = Color.GREEN
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 20f
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        path.reset()
        val rect = RectF(0f, 0f, w.toFloat(), h.toFloat())
        val inset = paint.strokeWidth
        rect.inset(inset, inset)

        path.addRoundRect(rect, 100f, 100f, Path.Direction.CW)
        length = PathMeasure(path, false).length
        intervals[1] = length
        intervals[0] = intervals[1]
        val effect: PathEffect = DashPathEffect(intervals, length)
        paint.setPathEffect(effect)
    }

    fun setProgress(progress: Int) {
        val effect: PathEffect = DashPathEffect(intervals, length - length * progress / 100)
        paint.setPathEffect(effect)
        invalidate()
    }

    override fun onDraw(canvas: Canvas) {
        canvas.drawPath(path, paint)
    }
}

原Activity使用代码

val ll = LinearLayout(this)
ll.orientation = LinearLayout.VERTICAL
val sb = SeekBar(this)
ll.addView(sb)
val v = V(this)
ll.addView(v)
setContentView(ll)

val sbcl: SeekBar.OnSeekBarChangeListener = object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
        v.setProgress(progress)
    }

    override fun onStartTrackingTouch(seekBar: SeekBar) {}
    override fun onStopTrackingTouch(seekBar: SeekBar) {}
}
sb.setOnSeekBarChangeListener(sbcl)

解决方案

方法1:手动构建从左上角开始的路径

默认Path.addRoundRect()的CW方向路径起点是左下角,我们可以手动构建路径,让起点从左上角开始,这样进度绘制的起始位置就会对应左上角。

修改自定义View的onSizeChanged方法:

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    path.reset()
    val rect = RectF(0f, 0f, w.toFloat(), h.toFloat())
    val inset = paint.strokeWidth
    rect.inset(inset, inset)
    val rx = 100f
    val ry = 100f

    // 手动构建从左上角开始的顺时针路径
    path.moveTo(rect.left + rx, rect.top)
    // 上边直边:从左上圆角结束点到右上圆角开始点
    path.rLineTo(rect.width() - 2 * rx, 0f)
    // 右上圆角:90度圆弧
    path.arcTo(rect.right - 2 * rx, rect.top, rect.right, rect.top + 2 * ry, 0f, 90f, false)
    // 右边直边:从右上圆角结束点到右下圆角开始点
    path.rLineTo(0f, rect.height() - 2 * ry)
    // 右下圆角:90度圆弧
    path.arcTo(rect.right, rect.bottom - 2 * ry, rect.right - 2 * rx, rect.bottom, 90f, 90f, false)
    // 下边直边:从右下圆角结束点到左下圆角开始点
    path.rLineTo(-(rect.width() - 2 * rx), 0f)
    // 左下圆角:90度圆弧
    path.arcTo(rect.left, rect.bottom - 2 * ry, rect.left + 2 * rx, rect.bottom, 180f, 90f, false)
    // 左边直边:从左下圆角结束点到左上圆角开始点
    path.rLineTo(0f, -(rect.height() - 2 * ry))
    // 左上圆角:90度圆弧
    path.arcTo(rect.left, rect.top, rect.left + 2 * rx, rect.top + 2 * ry, 270f, 90f, false)
    path.close()

    length = PathMeasure(path, false).length
    intervals[1] = length
    intervals[0] = intervals[1]
    val effect: PathEffect = DashPathEffect(intervals, length)
    paint.setPathEffect(effect)
}

修改后,路径起点为左上角的直边起始位置,拖动滑块时进度会从左上角开始绘制。

方法2:调整DashPathEffect的偏移量(不修改路径)

如果不想手动构建路径,可以计算从默认起点(左下角)到左上角的路径长度,调整DashPathEffect的偏移值,让绘制起始点定位到左上角。

  1. 在自定义View中添加变量存储左上角对应的偏移量:
private var startOffsetToTopLeft: Float = 0f
  1. 修改onSizeChanged方法,计算该偏移量:
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    path.reset()
    val rect = RectF(0f, 0f, w.toFloat(), h.toFloat())
    val inset = paint.strokeWidth
    rect.inset(inset, inset)
    val rx = 100f

    path.addRoundRect(rect, rx, rx, Path.Direction.CW)
    val pathMeasure = PathMeasure(path, false)
    length = pathMeasure.length

    // 计算从左下角到左上角的路径长度:右下直边 + 右下圆角 + 右上直边 + 右上圆角
    val rightEdgeLength = rect.width() - rx
    val cornerArcLength = Math.PI * rx / 2
    val topEdgeLength = rect.height() - rx
    startOffsetToTopLeft = (rightEdgeLength + cornerArcLength + topEdgeLength + cornerArcLength).toFloat()

    intervals[1] = length
    intervals[0] = intervals[1]
    // 初始偏移设置为左上角位置,保证初始状态隐藏
    val effect: PathEffect = DashPathEffect(intervals, startOffsetToTopLeft)
    paint.setPathEffect(effect)
}
  1. 修改setProgress方法,调整偏移量:
fun setProgress(progress: Int) {
    // 计算目标偏移量:从左上角开始,根据进度反向偏移
    val targetOffset = startOffsetToTopLeft - length * progress / 100f
    // 处理偏移量为负数的情况,取模保证在路径长度范围内
    val finalOffset = if (targetOffset < 0) targetOffset + length else targetOffset
    val effect: PathEffect = DashPathEffect(intervals, finalOffset)
    paint.setPathEffect(effect)
    invalidate()
}

此方法无需修改原路径结构,仅通过偏移调整就能让进度从左上角开始绘制。


内容的提问来源于stack exchange,提问作者Robin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 02:08:13