Search code examples
canvaspathandroid-jetpack-composeandroid-canvaslinechart

Blend Modes does not work as expected in Jetpack Compose


I'm trying to implement a line chart with points(as circles) on it. But regardless of which Blend Mode I'm using circles reveals line's intersection on it. Here is a picture of actual result:

enter image description here

I want my circle to be in a solid color. Here is my implementation:

path.apply {
            data.forEach { value ->

                val textResult = textMeasurer.measure(value.toString())
                val textOffsetX = -20f - textResult.firstBaseline

                yCursor = graphDepth - ((value - data.min()) * oneDegree)
                val textOffsetY = yCursor - textResult.lastBaseline / 2

                drawText(textResult, color = graphStyle.textColor, topLeft = Offset(textOffsetX, textOffsetY))
                drawLine(
                    Color.Gray,
                    start = Offset(0f, yCursor),
                    end = Offset(xCursor, yCursor),
                    strokeWidth = 3f,
                    pathEffect = PathEffect.dashPathEffect(
                        intervals = floatArrayOf(
                            10f,
                            5.dp.toPx()
                        )
                    )
                )
                moveTo(xCursor, yCursor)
                xCursor += oneInterval
            }
            xCursor = 0f
            yCursor = graphDepth - ((data.first() - data.min()) * oneDegree)
        }

        path.apply {
            moveTo(0f, yCursor)
            data.forEach { value ->

                yCursor = graphDepth - ((value - data.min()) * oneDegree)
                lineTo(xCursor, yCursor)
                drawLine(Color.Gray, start = Offset(xCursor, 0f), end = Offset(xCursor, graphDepth))
                moveTo(xCursor, yCursor)
                xCursor += oneInterval
            }
            xCursor = 0f
            yCursor = graphDepth - ((data.first() - data.min()) * oneDegree)
        }

        val circlePath = Path().apply {
            moveTo(0f, yCursor)
            data.forEach { value ->

                yCursor = graphDepth - ((value - data.min()) * oneDegree)
                drawCircle(
                    color = graphStyle.jointColor,
                    radius = graphStyle.jointRadius,
                    center = Offset(xCursor, yCursor),
                )
                moveTo(xCursor, yCursor)
                xCursor += oneInterval
            }
        }

        drawPath(
            path,
            color = graphStyle.lineColor.copy(alpha = 0.5f),
            style = Stroke(width = graphStyle.lineStroke),
            blendMode = BlendMode.Clear
        )

        drawPath(
            circlePath,
            color = graphStyle.jointColor,
        )


In which part, what I'm missing ? Can you help me with that ?

Thanks in advance.


Solution

  • For blendModes to work in Jetpack Compose you need draw an offscreen buffer.

    This can be done in 3 ways. By setting alpha less than 1f, Modifier.graphicsLayer{compositingStrategy = CompositingStrategy.Offscreen }

    Or saving and restoring layer with

    private fun DrawScope.drawWithLayer(block: DrawScope.() -> Unit) {
        with(drawContext.canvas.nativeCanvas) {
            val checkPoint = saveLayer(null, null)
            block()
            restoreToCount(checkPoint)
        }
    }
    

    Good thing about this solution is you can set which section you wish to apply blendModes or which drawings will be subject to blendModes while other 2 methods makes your Composable fully drawn with blendModes.

    @Preview
    @Composable
    fun BlendModeSample() {
        Canvas(
            modifier = Modifier.fillMaxSize().background(Color.DarkGray)
                .graphicsLayer {
                    compositingStrategy = CompositingStrategy.Offscreen
                }
    
        ) {
            drawWithLayer {
                drawLine(
                    color = Color.White,
                    start = Offset(100f, 100f),
                    end = Offset(300f, 100f),
                    strokeWidth = 10f
                )
    
                drawLine(
                    color = Color.White,
                    start = Offset(340f, 100f),
                    end = Offset(540f, 100f),
                    strokeWidth = 10f
                )
    
                drawCircle(
                    color = Color.Red,
                    center = Offset(320f, 100f),
                    blendMode = BlendMode.Clear,
                    radius = 50f
                )
            }
    
            drawCircle(
                color = Color.Red,
                center = Offset(320f, 100f),
                radius = 50f,
                style = Stroke(10f)
            )
    
        }
    }
    

    By applying BlendMode.Clear inside layer you remove any line section inside the circle while you can draw a circle with stroke at same position without any blend mode being applied

    Other option when you use paths is to use clipPath(pathToClip) {} to clip anything inside this path or androidx.compose.ui.graphics.Path().op().