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

How to apply PathEffect without using Stroke with Jetpack Compose?


I'm building a gooey effect as in gif below by combining PathEffects but as far as i know PathEffect can be applied to Stroke style only. Is there a Compose way to apply PathEffect while filling circles?

enter image description here

val discretePathEffect = DiscretePathEffect(pathMeasure.length / segmentCount, 0f)
val cornerPathEffect = PathEffect.cornerPathEffect(50f)


val chainPathEffect = PathEffect.chainPathEffect(
    outer = cornerPathEffect,
    inner = discretePathEffect.toComposePathEffect()
)

pathLeft.op(pathLeft, pathRight, PathOperation.Union)
pathMeasure.setPath(pathLeft, true)


drawPath(
    path = pathLeft,
    brush = brush,
    style = Stroke(
        4.dp.toPx(),
        pathEffect = chainPathEffect
    )
)

I need filled circles instead of stroke


Solution

  • I solved this using Compose paint

    val paint = remember {
        Paint()
    }
    

    and drawing with

    with(drawContext.canvas) {
        this.drawPath(
            newPath,
            paint
        )
    }
    

    enter image description here

    full implementation

    @Composable
    private fun GooeyEffectSample2() {
    
        val pathDynamic = remember { Path() }
        val pathStatic = remember { Path() }
    
        /**
         * Current position of the pointer that is pressed or being moved
         */
        var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
    
        val segmentCount = 20
        val pathMeasure = remember {
            PathMeasure()
        }
    
        val modifier = Modifier
            .pointerInput(Unit) {
                detectDragGestures { change, _ ->
                    currentPosition = change.position
                }
            }
            .fillMaxSize()
    
        val paint = remember {
            Paint()
        }
    
        var isPaintSetUp by remember {
            mutableStateOf(false)
        }
    
        Canvas(modifier = modifier) {
            val center = size.center
    
            val position = if (currentPosition == Offset.Unspecified) {
                center
            } else currentPosition
    
            pathDynamic.reset()
            pathDynamic.addOval(
                Rect(
                    center = position,
                    radius = 150f
                )
            )
    
            pathStatic.reset()
            pathStatic.addOval(
                Rect(
                    center = Offset(center.x, center.y),
                    radius = 100f
                )
            )
    
            pathMeasure.setPath(pathDynamic, true)
    
            val discretePathEffect = DiscretePathEffect(pathMeasure.length / segmentCount, 0f)
            val cornerPathEffect = CornerPathEffect(50f)
    
    
            val chainPathEffect = PathEffect.chainPathEffect(
                outer = cornerPathEffect.toComposePathEffect(),
                inner = discretePathEffect.toComposePathEffect()
            )
    
            if (!isPaintSetUp) {
    
                paint.shader = LinearGradientShader(
                    from = Offset.Zero,
                    to = Offset(size.width, size.height),
                    colors = listOf(
                        Color(0xffFFEB3B),
                        Color(0xffE91E63)
                    ),
                    tileMode = TileMode.Clamp
                )
                isPaintSetUp = true
                paint.pathEffect = chainPathEffect
            }
    
            val newPath = Path.combine(PathOperation.Union, pathDynamic, pathStatic)
    
            with(drawContext.canvas) {
                this.drawPath(
                    newPath,
                    paint
                )
            }
        }
    }