Draw a Cubic Bézier Curve on Android

Android Bezier Curve

In Android development, Canvas can be used to draw custom views. If you would like to draw a smooth curve, you can use the bézier curve logic. Here is a simple example that displays how to draw a cubic bézier curve on Android.

Bézier Curve

Since you are already here reading about “how to draw a cubic bézier curve on Android,” I assume you know the definition of bézier curves. Therefore, I would not cover that part in this post. However, if you are not familiar with the idea, please ask the internet and she will give you a lot of pages with easy-to-understand explanations.

Cubic Bézier Curve Drawing App

Now, let's build an example app that dynamically draws a bézier curve with SeekBars that changes the coordinates of control points. This would help us intuitively understand how these control points affect the shape of the curve.

Bezier Curve Demo

App Implementation

Let's go over the components and their implementations step by step.

Components

  • MainActivity: an Activity that hosts BezierSampleFragment
  • BezierSampleFragment: a Fragemnt that hosts BezierView
  • BezierViewModel: a ViewModel that stores SeekBar progress, and notifies the coordinates of the control points to BezierView
  • BezierView: a custom View that draws a cubic bézier curve

MainActivity is here only for hosting BezierSampleFragment, so let me omit the code explanation.

Statically Draw a Bézier Curve with BezierView

First of all, let's create a custom View that draws a curve statically. This is a class that extends View, and we will make it draw a curve in onDraw with Canvas.

We are going to have four control points, and their coordinates are expressed as percentage ratios relative to the size of the View. Let's create Point member variables: p1 as the starting point, p2 and p3 are the mid control points, and p4 is the ending point. The P1 and P4 will be fixed at the locations 15% away from the left, right, and bottom sides respectively. By the way, the x value starts from the left side and y from the top in the Android View coordinate system.

We also add the Path member variable instance that holds a bézier obit data since we are using Canvas.drawPath for drawing the curve. Let's create a function drawBezier which is called from onDraw. We are adding Point and Path member variables to avoid instantiating classes inside onDraw. Though we often see various complex and heavy processes taking place in onDraw, it is recommended to keep it as light as possible because onDraw could be called numerous times in a very short period of time.

Also, let's not forget to add the Paint for the curve drawing.

Here is the drawBezier function. A bézier orbit is defined in the Path instance. First, calling reset clears the stored orbit. Second, passing p1 coordinates to moveTo sets the starting point. Third, cubicTo takes the remaining points p2 - p4 in order to create a bézier orbit. Finally, draw with the paint. This function is exactly a pure one, but it refers to member variables width, height and Paint since they would not be frequently updated. The width and height are of BezierView itself, and they won't be 0 at this point since the calculation is done by the time onDraw is called. As you can see, absolute coordinate values are passed to the functions by multiplying width and height by the x and y percentage ratios.

This would draw a bézier curve, but let's also draw straight guide lines between the control points.

For this, we will create a function drawPoints and draw each circle in a different color. We are also adding a FILL Paint instance to call drawCircle with. Not to forget instantiating it as a member variable beforehand as it's used in onDraw.

Similarly, let's add drawGuideLines to draw grey dashed lines between points. Dashed lines can be drawn by setting DashPathEffect to Paint.pathEffect.

Finally, call them from onDraw, and here we have a custom View that statically draws a cubic bézier curve.

BezierView.kt - Static Version

Add BezierView to BezierSampleFragment

Let's see if it draws the bézier curve correctly by adding it to the Fragment.

fragment_bezier.xml

BezierSampleFragment.kt

Result

Bezier Curve Fixed

Success! We have the curve drawn as intended.

Next, we will implement SeekBar and ViewModel to changing the positions of control points p2 and p3 possible, and have it dynamically draw a curve.

Add SeekBar to the Layout

First, let's add four SeekBars on ConstraintLayout to control the x and y of P2 and P3 respectively. In order to achieve relatively intuitive UX, we will paint the P2 control red and P3 green.

fragment_bezier.xml - with SeekBars

Store SeekBar Progress in ViewModel

BezierViewModel is the data store for SeekBar progress. For now, let's keep it simple with getters and setters for LiveData, and pass initial values to MutableLiveData constructors.

BezierViewModel.kt

Next, connect this ViewModel and SeekBars on BezierSampleFragment. The VM can be accessed with activityViewModels from Fragment. In onViewCreated, have each SeekBar observe their progress stored in the VM, and notify the VM with progress changes from their listeners.

At this point, we have SeekBar connected and their progress values stored in the VM. The next step is to reflect these values on the control points of BezierView.

As I mentioned before, p1 - p4 Points of BezierView each has percentage values in 0 - 1f Float. However, SeekBar progress is expressed as 0 - 100% percentage in Int. So, Transformations is here to transform these LiveData values. On a side note, although the argument of the lambda of Transformations.map here would not be null in this implementation, we are making it null-safe just in case because passing null to MutableLiveData.postValue is technically possible.

Also, let's add an Extension for Int to convert percentage to a 1/100 Float value since we are using Kotlin.

BezierViewModel.kt - With Transformations

Now, observe these transformed Float values on BezierView init, and call invalidate upon changes to redraw itself. We will use Activity for the scope of the ViewModel and Observer to align with the Fragment. This will create a flow where each Observer receives a Float value when the SeekBar progress changes, and the bézier curve gets redrawn with updated Point coordinates.

Result

Bezier Curve Dynamic

Here we have an app that draws a bézier curve dynamically.

BezierView.kt - Dynamic Version

TL;DR

Drawing a cubic bézier curve can be achieved by using Path.cubicTo.

However, this example includes how to dynamically change the curve using ViewModel, LiveData, and Transformations. So, I hope it would also be helpful for you to build an MVVM architecture in real-life development.

References

COPYRIGHT © 2023 Kohei Ando