Draw some paint and control ImageView with Path.

class BusLineVue: ConstraintLayout {

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

    private val busIV = ImageView(context)

    private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val textRect = Rect(0, 0, 0, 0)

    private val path = Path()
    private val pathMeasure get() = PathMeasure(path, false)
    private var busStopData: List<Triple<Pair<Path, String>, Pair<PointF, RectF>, Paint>> = listOf()

    private val margin = 50f
    private var progress = 0f

    init {
        setWillNotDraw(false)

        busIV.setImageResource(androidx.appcompat.R.drawable.btn_checkbox_checked_mtrl)
        val layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
        layoutParams.width = 80
        layoutParams.height = 80
        busIV.layoutParams = layoutParams
        busIV.visibility = View.INVISIBLE
        addView(busIV)

        bgPaint.color = Color.parseColor("#000000")
        bgPaint.style = Paint.Style.STROKE
        bgPaint.strokeWidth = 4f

        linePaint.color = Color.parseColor("#1DDB16")
        linePaint.style = Paint.Style.STROKE
        linePaint.strokeWidth = 4f

        textPaint.color = Color.parseColor("#000000")
        textPaint.textSize = 40f

        post {
            val mWidth = measuredWidth.toFloat()
            val mHeight = measuredHeight.toFloat()

            // create path
            path.reset()
            path.moveTo(mWidth / 2f, margin)
            path.arcTo(margin, margin, mHeight - margin, mHeight - margin, 270f, -180f, false)
            path.arcTo(mWidth - mHeight - margin, margin, mWidth - margin, mHeight - margin, 90f, -180f, false)
            path.close()

            // create bus stop data
            busStopData = floatArrayOf(0f, 0.25f, 0.5f, 0.75f).mapIndexed { index, value ->
                val points = FloatArray(2)
                pathMeasure.getPosTan(pathMeasure.length * value, points, null)
                val pointF = PointF(points[0], points[1])
                val path = Path()
                val rectF = RectF(pointF.x - 20f, pointF.y - 20f, pointF.x + 20f, pointF.y + 20f)
                path.addRect(rectF, Path.Direction.CCW)
                val text =  when(index) {
                    0 -> "정류장 1"
                    1 -> "정류장 2"
                    2 -> "정류장 3"
                    else -> "정류장 4"
                }
                val stopPaint = Paint(Paint.ANTI_ALIAS_FLAG)
                stopPaint.color = Color.parseColor("#000000")
                stopPaint.style = Paint.Style.FILL
                Triple(Pair(path, text), Pair(pointF, rectF), stopPaint)
            }

            invalidate()
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // draw background
        canvas.drawPath(path, bgPaint)
        // draw line
        canvas.drawPath(path, linePaint)
        // draw stop
        busStopData.forEachIndexed { index, data ->
            canvas.drawPath(data.first.first, data.third)
            // draw stop name
            textPaint.getTextBounds(data.first.second, 0, data.first.second.length, textRect)
            when(index) {
                0 -> canvas.drawText(data.first.second, data.second.first.x - textRect.width() / 2, data.second.first.y + textRect.height() * 2f, textPaint)
                1 -> canvas.drawText(data.first.second, data.second.first.x + margin, data.second.first.y + textRect.height() * 0.5f, textPaint)
                2 -> canvas.drawText(data.first.second, data.second.first.x - textRect.width() / 2, data.second.first.y - textRect.height() * 1.5f, textPaint)
                else -> canvas.drawText(data.first.second, data.second.first.x - (textRect.width() + margin), data.second.first.y + textRect.height() * 0.5f, textPaint)
            }
        }
    }

    override fun dispatchDraw(canvas: Canvas) {
        super.dispatchDraw(canvas)
        // draw bus
        val pos = FloatArray(2)
        val tan = FloatArray(2)
        pathMeasure.getPosTan(pathMeasure.length * progress, pos, tan)
        val pointF = PointF(pos[0], pos[1])
        val angle = atan2(tan[1].toDouble(), tan[0].toDouble())

        canvas.translate(pointF.x - 40, pointF.y - 40)
        canvas.rotate(Math.toDegrees(angle).toFloat() - 90, 40f, 40f)
        busIV.draw(canvas)

        if (busStopData.isNotEmpty()) {
            val zeroStop = RectF(busStopData[0].second.second.left, busStopData[0].second.second.top, busStopData[0].second.second.right - 20f, busStopData[0].second.second.bottom)
            if (zeroStop.contains(pointF)) { busStopData.forEachIndexed { index, _ -> busStopData[index].third.color = Color.parseColor("#000000") } }

            if (busStopData[1].second.second.contains(pointF)) { busStopData[1].third.color = Color.parseColor("#FF0000") }
            if (busStopData[2].second.second.contains(pointF)) { busStopData[2].third.color = Color.parseColor("#FF0000") }
            if (busStopData[3].second.second.contains(pointF)) { busStopData[3].third.color = Color.parseColor("#FF0000") }
        }
    }

    fun set(progress: Float) {
        val totalDist = pathMeasure.length
        linePaint.pathEffect = DashPathEffect(floatArrayOf(totalDist, totalDist), totalDist * (1f - progress))
        this.progress = progress
        invalidate()
    }

}