Best Tutorial for Android Jetpack Compose

Android App Development

Stay ahead with the latest tools, trends, and best practices in Android development

Learn Cubic Bézier curves in Jetpack Compose

Learn Cubic Bézier curves in Jetpack Compose - Coding Bihar
Learn Cubic Bézier curves in Jetpack Compose
A Cubic Bézier curve is a type of curve used in graphics and design, defined by four control points. It is one of the most commonly used Bézier curves, especially in vector graphics, animation, and UI design. The curve is calculated using the following formula:

Formula for Cubic Bézier Curve

The cubic Bézier curve is defined by four points:
  1. \(P_0\)  (start point)
  2. \(P_1\) (first control point)
  3. \(P_2\) (second control point)
  4. \(P_3\) (end point)
The cubic Bézier curve B(t) at any value of t (where t ranges from 0 to 1) is calculated using the formula:
\(B(t) = (1-3)^{3}.P_0+3(1-t)^{2}.t.P_1+3(1-t).t^{2}.P_2+t^{3}.P_3\)

Where:

  • \(P_0\) is the starting point.
  • \(P1\) and \(P_2\) are control points, which influence the curve's direction.
  • \(P_3\) is the endpoint.
  • t is a value between 0 and 1 that determines the position on the curve.

How Cubic Bézier curves work:

  • Start Point \(P_0): ​This is where the curve begins.
  • Control Points \(P_1\) and \(P_2): These points "pull" the curve towards them, determining the direction and steepness of the curve.
  • End Point \(P_3\): This is where the curve ends.
  • t: The parameter that moves from 0 (start) to 1 (end), which determines the curve's progression along the line between the start and end points.

How to draw heart shapes using Jetpack Compose?

1. Drawing a Static Cubic Bézier in Canvas

Compose’s Canvas gives you a Path API with a cubicTo(...) call exactly for Béziers. Here’s a simple Composable that draws our example S-curve from 
(0,0)  to (300,0) with control points at (0,150) and (300,150):
package com.cdingbihar.StaticBezierDemo

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp

//  The Composable
@Composable
fun StaticBezierDemo() {
    Box(Modifier.fillMaxSize()) {
        // A fixed-size canvas to draw into
        Canvas(modifier = Modifier.size(320.dp)) {
            // `size` is now in scope here:
            val p0 = Offset(10f, size.height - 10f)
            val p1 = Offset(10f, 10f)
            val p2 = Offset(size.width - 10f, 10f)
            val p3 = Offset(size.width - 10f, size.height - 10f)

            // Build the path
            val path = Path().apply {
                moveTo(p0.x, p0.y)
                cubicTo(
                    p1.x, p1.y,  // first control point
                    p2.x, p2.y,  // second control point
                    p3.x, p3.y   // end point
                )
            }

            // Draw the curve
            drawPath(
                path = path,
                color = Color.Blue,
                style = Stroke(width = 5f, cap = StrokeCap.Round)
            )

            // Draw the control points
            listOf(p0, p1, p2, p3).forEach { point ->
                drawCircle(Color.Red, radius = 8f, center = point)
            }
        }
    }
}
  • moveTo() jumps to the start P0
  • cubicTo(cx1, cy1, cx2, cy2, x3, y3) adds your Bézier segment.
  • Finally we stroke it in blue and mark the four defining points in red so you can see how control handles pull the curve.
Static Bezier Demo

2. Animating the Control Handles

One of the biggest joys of Compose is built-in animation. Let’s tween our control points so you can watch the shape breathe:
  • We use rememberInfiniteTransition() + animateFloat(...) to shuttle progress back and forth.
  • p1.x and p2.x shift based on that progress, so the curve’s bend “breathes.”
  • Dashed gray lines connect handle pairs (start→control1, control2→end) just like vector‐editing UIs.
package com.cdingbihar.aniimatedbezier

import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp

@Composable
fun AnimatedBezier() {
    val modifier = Modifier.size(320.dp)
    // Animate from “flat” to “arched” control points
    val progress = rememberInfiniteTransition()
        .animateFloat(
            initialValue = 0.1f,
            targetValue  = 0.9f,
            animationSpec = infiniteRepeatable(
                animation = tween(2000, easing = FastOutSlowInEasing),
                repeatMode = RepeatMode.Reverse
            )
        ).value

    Canvas(modifier = modifier) {
        val w = size.width
        val h = size.height

        val p0 = Offset(10f, h - 10f)
        // control points slide horizontally as `progress` changes
        val p1 = Offset(10f + progress * w * 0.4f, 10f)
        val p2 = Offset(w - 10f - progress * w * 0.4f, 10f)
        val p3 = Offset(w - 10f, h - 10f)

        val path = Path().apply {
            moveTo(p0.x, p0.y)
            cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
        }

        drawPath(path, color = Color.Magenta, style = Stroke(6f, cap = StrokeCap.Round))

        // draw handles
        drawLine(Color.Gray, p0, p1, strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f,10f),0f))
        drawLine(Color.Gray, p2, p3, strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f,10f),0f))
        listOf(p0, p1, p2, p3).forEach { drawCircle(Color.Green, 6f, it) }
    }
}
Animated Bezier


3. Why This Matters in Compose

Custom Shapes & Clipping

You can use cubic Béziers to define arbitrary clip paths (clipPath { … }), letting you craft wavy cards, speech bubbles, or any freeform container.

Vector‐Drawable Interop

Compose’s Path is essentially the same model as Android’s VectorDrawable pathData. If you’ve created an SVG with <path d="M … C …" />, you can translate those commands directly into Compose.

Motion & Gestures

You can let users drag control points at runtime—just track touch events, update p1/p2, and recomposite. It’s the basis for interactive curve editors or sketch apps.

4. Putting It All Together

You might combine these pieces into a little UI:
package com.cdingbihar.beziereditor

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt

@Composable
fun BezierEditor() {
    var handle1 by remember { mutableStateOf(Offset(100f,  50f)) }
    var handle2 by remember { mutableStateOf(Offset(200f,  50f)) }

    Box(Modifier.fillMaxSize().systemBarsPadding()) {
        Canvas(Modifier.matchParentSize()) {
            // draw cubic with dynamic handles
            Path().apply {
                moveTo(50f, size.height - 50f)
                cubicTo(
                    handle1.x, handle1.y,
                    handle2.x, handle2.y,
                    size.width - 50f, size.height - 50f
                )
            }.also { path ->
                drawPath(path, Color.Cyan, style = Stroke(8f))
            }
        }

        // Draggable handle UIs
        Handle(handle1) { handle1 = it }
        Handle(handle2) { handle2 = it }
    }
}

@Composable
fun Handle(pos: Offset, onDrag: (Offset) -> Unit) {
    // A tiny, draggable circle
    Box(
      Modifier
        .offset { IntOffset(pos.x.roundToInt() - 20, pos.y.roundToInt() - 20) }
        .size(40.dp)
        .pointerInput(Unit) {
          detectDragGestures { _, dragAmount ->
            onDrag(pos + Offset(dragAmount.x, dragAmount.y))
          }
        }
        .background(Color.Yellow, shape = CircleShape)
    )
}
Bezier Editor 1

Bezier Editor 2

Bezier Editor 3


This mini-editor lets you grab the two yellow handles and see the Bézier curve update instantly. Under the hood Compose’s recomposition and Canvas are doing exactly the weighted-blend math we walked through earlier—only you never have to write (1 - t)³ by hand!

Special Message

Welcome to Coding