Search code examples
androidkotlinandroid-jetpack-composeandroid-jetpack-compose-canvas

How to draw transparent arc with line corner end in jetpack compose


I want to draw transparent arc after color gradient radius end. It sound confusing. I have progress bar in which I have end the line at X position. After that I want to show transparent arc space at X position with Line Gradient. I am trying to use drawArc, but it's not working correctly.

@Composable
fun DrawProgressBar() {
    val activity = LocalContext.current as AppCompatActivity
    val rangeComposition = RangeComposition()
    val itemLst = rangeComposition.bpExplained
    val boxSize = 30.dp
    val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
    val progressBarPointer = rangeComposition.findReadingWithPointer(142, 90).second
    Box(
        modifier = Modifier
            .background(Color.White)
            .height(height = boxSize)
    ) {
        Canvas(
            modifier = Modifier.fillMaxSize()
        ) {
            val strokeWidth = 8.dp
            val canvasWidth = size.width
            val canvasHeight = size.height
            val strokeWidthPx = density.run { strokeWidth.toPx() }
            drawLine(
                start = Offset(x = 0f, y = canvasHeight / 2),
                end = Offset(x = canvasWidth, y = canvasHeight / 2),
                color = Color.Gray,
                strokeWidth = strokeWidthPx,
                cap = StrokeCap.Round,
            )
            val progressBarPointerInPixel = (progressBarPointer / 100f) * canvasWidth
            activity.logE("progressBarPointerInPixel $progressBarPointerInPixel")
            drawLine(
                brush = brush,
                start = Offset(x = 0f, y = canvasHeight / 2),
                end = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2),
                strokeWidth = strokeWidthPx,
                cap = StrokeCap.Round,
            )
            drawArc(
                topLeft = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2),
                size = Size(8.dp.toPx(), strokeWidthPx),
                color = Color.Cyan,
                startAngle = -90f,
                sweepAngle = 180f,
                useCenter = true
            )
            itemLst.forEachIndexed { index, rangeItem ->
                val endPointInPixel = (rangeItem.endPoint / 100f) * canvasWidth
                if (index != itemLst.lastIndex) {
                    drawLine(
                        start = Offset(x = endPointInPixel, y = 0F),
                        end = Offset(x = endPointInPixel, y = boxSize.toPx()),
                        color = Color.Black,
                        strokeWidth = 4.dp.toPx(),
                    )
                }
            }
        }
    }
}

Actual Output

enter image description here

Expected Output

enter image description here

You can find the source code of RangeComposition.kt.

UPDATE

After @GabrieleMariotti mention code I tried with white color and I see that there is white tiny vertical bar is there

fun DrawProgressBar() {
    val activity = LocalContext.current as AppCompatActivity
    val rangeComposition = RangeComposition()
    val itemLst = rangeComposition.bpExplained
    val boxSize = 30.dp
    val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
    val progressBarPointer = rangeComposition.findReadingWithPointer(142, 90).second
    Box(
        modifier = Modifier
            .background(Color.White)
            .height(height = boxSize)
    ) {
        Canvas(
            modifier = Modifier.fillMaxSize()
        ) {
            val strokeWidth = 8.dp
            val canvasWidth = size.width
            val canvasHeight = size.height
            val strokeWidthPx = density.run { strokeWidth.toPx() }
            val pathEffect = PathEffect.dashPathEffect(floatArrayOf(canvasHeight / 19, canvasHeight / 19), 0f)
            drawLine(
                start = Offset(x = 0f, y = canvasHeight / 2),
                end = Offset(x = canvasWidth, y = canvasHeight / 2),
                color = Color.Gray,
                strokeWidth = strokeWidthPx,
                cap = StrokeCap.Round,
            )
            val progressBarPointerInPixel = (progressBarPointer / 100f) * canvasWidth
            drawLine(
                color = Color.White,
                start = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2),
                end = Offset(x = progressBarPointerInPixel + strokeWidthPx / 2, y = canvasHeight / 2),
                strokeWidth = strokeWidthPx,
            )
            drawLine(
                brush = brush,
                start = Offset(x = 0f, y = canvasHeight / 2),
                end = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2),
                strokeWidth = strokeWidthPx,
                cap = StrokeCap.Round,
            )
            drawArc(
                topLeft = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2 - strokeWidthPx / 2),
                size = Size(strokeWidthPx, strokeWidthPx),
                color = Color.White,
                startAngle = -90f,
                sweepAngle = 180f,
                useCenter = true
            )
            itemLst.forEachIndexed { index, rangeItem ->
                val endPointInPixel = (rangeItem.endPoint / 100f) * canvasWidth
                if (index != itemLst.lastIndex) {
                    drawLine(
                        start = Offset(x = endPointInPixel, y = 0F),
                        end = Offset(x = endPointInPixel, y = boxSize.toPx()),
                        color = Color.Black,
                        strokeWidth = 1.2.dp.toPx(),
                        pathEffect = pathEffect
                    )
                }
            }
        }
    }
}

Result

enter image description here


Solution

  • In your arc you have to change the topLeft offset considering also the height offset due to the strokeWidthPx. Something like:

    topLeft = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2 - strokeWidthPx/2),
    

    Also you should add also a line from progressBarPointerInPixel to progressBarPointerInPixel + strokeWidthPx/2 due to the rounded corners.

    Something like:

        drawLine(
          //gray line
        )
    
        drawLine(
            color = Color.Cyan,
            start = Offset(x = progressBarPointerInPixel , y = canvasHeight / 2),
            end = Offset(x = progressBarPointerInPixel + strokeWidthPx/2, y = canvasHeight / 2),
            strokeWidth = strokeWidthPx,
        )
    
        drawLine(
            brush = brush,
            start = Offset(x = 0f, y = canvasHeight / 2),
            end = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2),
            strokeWidth = strokeWidthPx,
            cap = StrokeCap.Round,
        )
    
        drawArc(
            topLeft = Offset(x = progressBarPointerInPixel, y = canvasHeight / 2 - strokeWidthPx/2),
            size = Size(strokeWidthPx,strokeWidthPx),
            color = Color.Cyan,
            startAngle = -90f,
            sweepAngle = 180f,
            useCenter = true
        )
    

    enter image description here

    To achieve a transparent arc you can add the blendMode = BlendMode.DstOut to line+arc.It also requires to apply an alpha !=1F to the Canvas with graphicsLayer(alpha = 0.99f) Check the doc for more details about the blendMode.

    Canvas(
        modifier = Modifier.fillMaxSize().graphicsLayer(alpha = 0.99f)
        ) {
           
        drawLine(
           //gray line
        )
    
        
        drawLine(
            //...
            color = Color.Cyan,
            blendMode = BlendMode.DstOut
        )
    
        drawLine(
           //gradient
        )
    
        drawArc(
            blendMode = BlendMode.DstOut
        )
    
    }