Hi, I'm Rebecca.
In this video,
you will learn all about how to draw something custom in Compose.
Custom drawing is useful for when the built-in components
just don't cover exactly what your app needs.
Say, you have a custom graph that you want to draw,
or you want to create a fun flashlight effect in your app,
or even a complex Shader background.
It's all possible in Compose.
To get started with drawing in Compose,
we can use a drawing modifier,
such as modifier drawBehind.
This modifier allows us to draw content
behind our Composables contents.
For example,
if we wanted to draw a simple circle,
we can call drawCircle
inside drawBehind.
This results in a circle
that takes up the full size of the Composable.
It's worth noting that inside the drawBehind modifier,
we are in a DrawScope.
What is DrawScope?
DrawScope is a declarative, stateless drawing API
to draw shapes, paths, etc.
without needing to maintain your own state.
If you look at the interface definition of DrawScope,
you will see it has a bunch of methods used for drawing,
such as drawLine, drawRect, etc.
We can also get access
to the size and center of the drawing area.
These will be useful
when trying to adjust the sizing and placement of an item.
If we wanted a simple drawing,
we could use the Canvas Composable.
However, looking under the hood,
we can see that this is just a convenient wrapper
around modifier drawBehind.
There are several drawing modifiers
that also give you access to a DrawScope,
allowing you to draw with other Composables.
The different modifiers are:
drawBehind, which allows you to draw content
behind the contents of the Composable;
drawWithContent, which allows you to choose
when to call the content of your Composable--
this is useful
for when you want to rearrange when content is drawn,
for instance, in front or behind the drawing code--
and drawWithCache,
which caches the objects
until the size changes
or the state variables read inside change.
Each of these are very useful in their own regard,
but for now, we will stick with using Canvas
since our drawings are quite simple at the moment.
Right. We have our circle that we've drawn,
but we want to position it differently from where it is placed right now.
All of the drawing functions on DrawScope have default values
set for the size and position of the drawing commands.
These can be easily overridden when we need to change them,
but to know how to set the size and position,
we need to understand a bit more
about the coordinate system used in Compose.
The coordinate system used in Compose
is the same as the view system.
It has the [0, 0] point
in the top-left corner of the Composable,
and the bottom-right has the coordinates of width and height.
The y value increases
as it moves downward,
and the x value increases
as it moves to the right.
We also need to keep in mind
that all draw and layout calls
are performed in pixel values,
not dp.
Therefore, to draw things consistently across screens,
you should use dp
and convert to pixels before drawing.
Draw calls are also always relative
to the parent Composable.
If you're placed inside a box
that has a certain height and offset,
your coordinate system changes
to be relative to the parent.
The [0, 0] point is now at that offset,
and the width and height of the drawing area
is now the size of the parent Composable.
Back to our circle.
To change the position of the circle we have drawn on screen,
we can set the center point of the circle to have a different offset,
say, 20 dp in the x direction
and 100 dp in the y direction.
We can also change the radius of the circle
to only take up 60 dp
instead of the default of taking up the full size of the drawing area.
Great, we have a circle
that we now know how to size and position.
Let's look at what other transformation functions
are available to us.
DrawScope offers a few different transformations
that can be applied to the drawing commands.
For example,
if we wanted to scale up
everything that is drawn on our canvas,
we can use DrawScope scale function
with an x and y value
for how many times the item should be scaled.
In this case,
we're drawing a small circle with a 20 dp radius,
but we're scaling it 10 times horizontally
and 15 times vertically,
so it ends up as an oval
with 200 dp width
and 300 dp height.
To translate all the contents,
we can call translate
with a left and top offset
that will be applied to all the content drawn inside.
To apply rotation to content,
you can call rotate
with the amount of degrees to rotate the drawn content.
Another transformation is to perform an inset
to everything that is being drawn inside,
providing a horizontal and vertical pixel amount
that will effectively pad the lambda content.
If you want to apply multiple transformations to your content,
use the withTransform function
that applies transformations to the content,
such as translate, etc.
Now you understand the basics of drawing something in Compose.
Let's build something a little bit more practical,
like this shiny line graph.
First, let's start drawing the background of the graph.
We can see it has a number of vertical and horizontal lines.
The first step
is we set the background
to our purple background color
and then create a canvas
with a 3x2 aspect ratio
to keep it in the correct size.
Then we can draw the outline rectangle
for the whole graph
using a white color and a stroke.
This will take up the whole canvas area size.
Next up, we repeatedly called drawLine
for each vertical line in the graph
that we want to draw,
offsetting it by an equal amount each time.
We repeat the same things for horizontal lines,
calling repeat for each line that will be drawn horizontally
with a different start y value each time.
Now we have the background drawn for our graph,
we can go about drawing the actual line that represents the data.
We had the canvas Composable that we were using,
but now we are going to need to create a path object
and draw the line of the graph.
We don't want to be recreating this path object on every recomposition.
To avoid that,
we can switch to using the drawWithCache modifier,
which will take care of caching the objects
until the size of the drawing area changes.
It's worth noting that you should only use this
if you are creating objects in the draw functions.
Otherwise, it will create unnecessary lambdas.
Next, we create a path object.
We can call our function generatePath
that takes the graph data into it
as well as the size of the area that we are drawing into.
We will look at the definition of this function in a second,
but let's assume that we have this path object already.
To actually draw the path,
we now call drawPath
with the path, the color,
and the stroke width that we want the line to have.
We get a nice green line drawn,
but how is the actual path generated?
Looking at our generate path function,
it goes along the following lines:
It creates a path,
and for each data point in that list of points,
it adds a call to path.lineTo
to the path that we are generating,
and then it returns this path.
Great. Let's move on to the next step.
Now to get the area below the graph
filled with the green gradient,
we can create another path
where we add the original path
and then create a closed area to fill in.
To create this closed area,
we need to add two more lines
aligned to the bottom right of the graph
and then another line to the bottom left of the graph
where we started.
We then call close to close the path.
Then we call drawPath
with a green gradient brush that we've made.
Our graph looks great as it is,
but it would look way nicer
if the lines were smooth between the values.
Let's smooth the lines using path functions.
To do the smoothing,
all we need to do is change our generatePath function
to generate a path with curves in it.
We calculate our two control points
for each point that we want to add to our lines.
Instead of calling lineTo,
we call cubicTo
using these two control points,
giving us a smoother line
and a better-looking graph overall.
As a bonus--
because what's a Compose video without a small animation--
let's look at how we can animate the line
that is drawn on launch of the graph.
First, let's create an animatable object
that we will remember.
We then call animateTo
in our LaunchedEffect
so that this will only run once
on the launch of our Composable.
We now have a value
that will animate from 0 to 1.
Now, to use this value,
we can clip our paths
that are drawing on screen
using this animation value that we created.
We use the clipRect method
that will clip all the draw calls that are inside that lambda.
We set the right edge of the rectangle
to be a fraction of the width.
Because this animation progress value
animates from 0 to 1,
we will get a reveal animation happening over time.
Running this, we can see the graph area
is now drawn over time,
creating the reveal animation effect.
That's it for this video.
Take a look at the Compose graphic docs
to learn more about drawing in Compose.
Happy Composing!
♪ [music] ♪