Draw a Cubic Bézier Curve on Android
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.
App Implementation
Let's go over the components and their implementations step by step.
Components
MainActivity
: anActivity
that hostsBezierSampleFragment
BezierSampleFragment
: aFragemnt
that hostsBezierView
BezierViewModel
: aViewModel
that storesSeekBar
progress
, and notifies the coordinates of the control points toBezierView
BezierView
: a customView
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
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
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.