Graphics Using Jetpack Compose | Kodeco
[ad_1]
Android has a variety of graphics-based objects to place on the app’s screen. Some of these graphics objects include text, buttons and checkboxes. What if you wanted to draw custom graphics? Like rectangles, lines, triangles and other shapes? Video games, painting apps or chart drawing programs need custom graphics. When you want a nice avatar for your next game, custom graphics with Jetpack Compose will be the way to create it.
You draw custom graphics on a special view called a Canvas with a Paint interface. When working with Jetpack Compose, you use the Graphics API. The Graphics API uses an approach called the declarative programming paradigm. In this tutorial, you’ll learn this simpler way to use the Graphics API on Canvas.
In this tutorial, you’ll use Jetpack Compose to:
- Draw primitive shapes with custom graphics.
- Create complex custom graphics by combining graphics objects.
- Display text using Paint.
- Transform objects.
Getting Started
Start by using the Download Materials button at the top or bottom of this tutorial to download the project.
Open the project in Android Studio Bumblebee or later and get familiar with the files. You’ll notice a starter project and a final project. Open and run the starter project. The app – Pacman – contains a blank, white screen.
Humble beginnings so far, right? If you’re wondering about the end result of the project, open and run the final project. You’ll see the Pacman screen:
Hopefully, seeing the final app will get you fired up to start drawing some awesome Pacman graphics. So with that, waka, waka, chomp, chomp. Time to get to it!
Creating Jetpack Compose Custom Graphics
You draw graphics like buttons, text fields and pickers by placing them on a view. “Graphics” in this tutorial refers to custom drawings like rectangles, triangles and lines. The graphics here, called primitives, aren’t like sophisticated shapes like buttons. You could create graphics using bitmap or SVG, but the Graphics API in Android gives you an alternative way to do it. Instead of drawing graphics using tools like Adobe Illustrator and importing them as bitmap or SVG, you can create raw graphics directly through code.
Using only code, you still create a bitmap with pixels and will see the same images on the Android screen. Your code uses Canvas to draw objects using a bitmap. Instead of putting certain colors in specific x and y locations, you use helper methods to draw common shapes like lines, rectangles and circles. Finally, to change the bitmap’s style and colors, you use the Paint interface.
Using Declarative Graphics API
The Graphics API has been in the Android SDK since its inception, API level 1, in 2008. What’s new is a declarative way to use this API: It makes managing Canvas and Paint an easy task. You don’t need to set method or other configurations on the Paint object. Instead, all the configuration and execution happen in one place: the composable function. Before Jetpack Compose, working with the Paint API required meticulous detail since code organization decisions could cause noticeable performance inefficiencies. Additionally, working with Canvas can be confusing. But with Compose, it’s a breeze to create graphics.
Understanding Canvas
What is Canvas?
To draw an object on the Android screen, first you need a Drawable to hold your drawing. Drawings are composed of pixels. If you’ve worked in Android very long, you’ll know about Drawable. You use this class if you want to display an image. Canvas holds the methods to draw shapes. So, with Drawable, you override the draw method. The draw method accepts Canvas as the argument. This connection allows you to draw shapes using code in Drawable to put them on the Canvas.
What can you do with Canvas?
Canvas allows you to draw many primitive shapes, from circles to clipping the shapes. You could say that Canvas is an abstraction of Drawable. You load up an image with Drawable by inflating SVG or PNG files. Inflating doesn’t need Canvas. But if you wanted to draw a primitive shape dynamically, you’d use Canvas.
Think of Canvas as a layer on top of Drawable where you could draw a variety of shapes.
Creating a Canvas
Creating a Canvas is straightforward. In the starter project, open MainActivity.kt and check out CanvasApp
:
Canvas(modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
) {
...
}
Canvas accepts a modifier that lets you modify the Canvas’s size, for example. In this example, you set the size to the maximum size of the parent.
When you ran the starter project, you didn’t see anything, but it’s time to change that. Look at the first two lines inside the Canvas block:
val canvasWidth = size.width
val canvasHeight = size.height
Inside the Canvas block, you could query the size of Canvas from size
. Remember the modifier above that extends to the maximum width and height? That’s the size of Canvas.
Drawing on a Canvas With Jetpack Compose
Following the first two lines is a call to drawBlackScreen
:
drawBlackScreen(scope = this, canvasWidth, canvasHeight)
Go inside drawBlackScreen
. It’s empty right now. Put the following code inside it:
scope.drawRect(Color.Black,
size=Size(canvasWidth, canvasHeight))
As its name suggests, drawRect
draws a rectangle. It’s worth noting that thoughtful method names are a tremendous help in code development. You know what drawRect
does by its name: It draws a rectangle. But what are its parameters? The first is the color of the rectangle and the second parameter is the size of the rectangle.
By calling Size
, Android Studio assists you in adding the required import, androidx.compose.ui.geometry.Size
.
Build and run the app. You’ll see a black rectangle over the full screen:
In your usage, you omitted the Paint
object argument, but you could supply one to change the style of the drawn rectangle. You’ll see how to construct Paint later. You also omitted the position argument. This means you used the default value for the position, which is 0 for x and y coordinates. Other parameters define color and size, which are common to all objects. Other object methods require different parameters according to the object shape. drawCircle
needs an argument for radius, but the line object doesn’t.
Using Modifier
You’ve seen Modifier
in the Canvas function. But you have other arsenals as well. Suppose you want to add some padding. Change the Modifier
inside Canvas to:
Canvas(modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(50.dp)
) {
Don’t forget to import padding
. Each method on the modifier returns an updated Modifier instance. So by chaining the method calls, you’re gradually building the Canvas. The order by which you call the methods matter.
Rebuild the project and run it. You’ll see the black screen now has white padding:
Now that you’ve seen how padding works for custom graphics in Jetpack Compose, you can remove that code.
Creating Objects With Jetpack Compose
It’s finally time to try to draw some shapes! You’ll see that each of these shapes has its own characteristics.
Drawing Lines
Now that you’ve created a rectangle, it’s time to create other shapes. You’ll start by drawing part of the Pacman maze. Go inside drawBlueLines
. You notice that there’s an annotation on top of this method, @OptIn(ExperimentalGraphicsApi::class)
. It’s needed because you use Color.hsv
, which is experimental. So what is this method? It gets a color that you’ll use to draw on Canvas. HSV (hue, saturation, value) is one of the color spaces besides RGB (red, green, blue). You can read more about HSV in Image Processing in iOS Part 1: Raw Bitmap Modification. Color.hsv
accepts three arguments: hue, saturation and value. The saturation and value ranges from 0 to 1. It’s a float which represents the percentage value.
In this method, you need to draw four lines. You’ve already got the positions defined for you.
Add the following code at // 2. Use the drawLine method
:
scope.drawLine(
blue, // 1
Offset(0f, line), // 2
Offset(canvasWidth, line), // 2
strokeWidth = 8.dp.value // 3
)
Here’s what is happening:
-
blue
is the color you got fromColor.hsv
. - These define the dot positions that make a line when connected. Each dot needs an
Offset
. Basically, it’s an object that accepts two values, the x and y positions. - This sets the width of the stroke. The higher the value, the thicker your line becomes. You define a line by two points. That’s why the method to draw a line needs two
Offset
arguments. It’s different from the method for drawing a rectangle.
Rebuild the project and run the app. You’ll see four blue lines:
Notice that you’ve drawn lines after drawing a rectangle — the order matters. If you draw lines first, then draw a rectangle, the big rectangle will cover your lines.
Drawing Circles
Next, you’ll draw a power pellet. The circle represents an object that, if eaten by Pacman, makes him immune to ghosts for a certain period of time. Go to // 3. Use the drawCircle method
, and add the following:
scope.drawCircle(purple, // 1
center = Offset(pacmanOffset.x + 600.dp.value, dotYPos), // 2
radius = radius) // 3
Here’s what this code does:
- This specifies the color of the circle.
- The
center
argument refers to the position of the center of the circle in Canvas. - The
radius
refers to how big your circle is.
As you can see, both methods — whether drawing a rectangle, line or circle — accept a color argument. However, they differ in other arguments because every shape is a bit different. All the methods accept an optional Paint object.
Build the project and run the app. You’ll see a purple circle:
With that, your power pellet is ready to give ol’ Blinky — spoiler — a run for his money! :]
Drawing Point Lines
In the Pacman video games, this line of points refer to the dots that Pacman needs to eat to finish the game. You can create all the points one by one, but you could also use a method to create a line consisting of points. Find // 4. Use the drawPoints method
, and add the following:
scope.drawPoints(points, // 1
PointMode.Points, // 2
purple, // 3
strokeWidth = 16.dp.value) // 4
This code defines:
- The list of
Offset
s where you defined the position of points. - The mode or style of the point. Here, you render small squares. There are other
PointMode
options. Try them out before moving on. Press Ctrl (or CMD on a Mac) + Space on your keyboard to see the other options. - Color.
- Line thickness.
Build the project and run the app. You’ll see a line of points:
Drawing Arcs
Now, here comes the most exciting part of the tutorial: drawing Pacman himself! Pacman is a not-quite-full circle. You call this shape a sector. You call a quarter of a circle an arc. Pacman looks like a circle with an arc taken out!
Below // 5. Use the drawArc method
within drawPacman
, add the following code:
scope.drawArc(
Color.Yellow, // 1
45f, // 2
270f, // 3
true, // 4
pacmanOffset,
Size(200.dp.value, 200.dp.value)
)
This code specifies:
-
Yellow
as the arc’s color. - Start angle, which refers to the bottom part of Pacman’s mouth.
- Sweep angle. Sum the start angle and the sweep angle, and you’ll get the position of the top part of the mouth. Zero degrees starts at the right side of the circle. If you think of the top as north and bottom as south, then zero degrees is in the west direction. You could change the start angle to zero and redraw Pacman to see the location of zero degrees.
- Whether you draw a line between the start angle and the end angle using the center. If not, you draw a direct line between the start and end angles. In your case, you want to use the center because you create a mouth by making a line from the start angle to the center and then from the center to the end angle.
Build the project and run the app. You’ll see Pacman:
You can see the difference between using the center or not in drawing an arc in the picture below:
Drawing Complex Shapes: Blinky the Ghost
The ghosts that chase your Pacman have a complex shape, and Jetpack Compose doesn’t have any “ghost” shapes in its custom graphics. :] So, you’ll draw a custom ghost shape. To do this, you need to divide a ghost into a few simple shapes and draw them each using the methods you’ve learned.
You can separate a ghost into different primitive shapes:
Drawing the Ghost’s Feet
Breaking down the ghost custom graphic, separate the feet. What do you see? Three arcs or half-circles lined up horizontally.
Go inside drawGhost
and add the following code:
val ghostXPos = canvasWidth / 4
val ghostYPos = canvasHeight / 2
val threeBumpsPath = Path().let {
it.arcTo( // 1
Rect(Offset(ghostXPos - 50.dp.value, ghostYPos + 175.dp.value),
Size(50.dp.value, 50.dp.value)),
startAngleDegrees = 0f,
sweepAngleDegrees = 180f,
forceMoveTo = true
)
it.arcTo( // 2
Rect(Offset(ghostXPos - 100.dp.value, ghostYPos + 175.dp.value),
Size(50.dp.value, 50.dp.value)),
startAngleDegrees = 0f,
sweepAngleDegrees = 180f,
forceMoveTo = true
)
it.arcTo( // 3
Rect(Offset(ghostXPos - 150.dp.value, ghostYPos + 175.dp.value),
Size(50.dp.value, 50.dp.value)),
startAngleDegrees = 0f,
sweepAngleDegrees = 180f,
forceMoveTo = true
)
it.close()
it
}
scope.drawPath( // 4
path = threeBumpsPath,
Color.Red,
style = Fill
)
By calling Rect
, Android Studio assists you in adding the required import, androidx.compose.ui.geometry.Rect
.
-
arcTo
is similar todrawArc
above: thestartAngleDegrees
andsweepAngleDegrees
arguments are like start and sweep angles where the first argument is the rectangle that defines or bounds the size of the arc. The last argument moves thePath
point to the end of the path before drawing another arc. Otherwise, you’d always draw other arcs from the same starting position or beginning of the first arc. - You did exactly the same as above, only you’re starting at the end of the first one.
- For the last leg, you start at the end of the second leg.
-
path
argument is the path you’ve created, and the second argument is the color of your path. The third argument,fill
, is whether you should fill the path with the selected color.
Rebuild the project and run the app. You’ll see the ghost’s feet:
Note: Instead of using drawPath
, you could use drawArc
three times. Experiment with drawArc
and see which one is more convenient for you.
Drawing the Ghost’s Body
Now, you’ll draw a rectangle as the main part of the ghost’s body. You already know how to build a rectangle, so add the following code at the bottom of drawGhost
:
scope.drawRect(
Color.Red,
Offset(ghostXPos - 150.dp.value, ghostYPos + 120.dp.value),
Size(150.dp.value, 82.dp.value)
)
Rebuild the project and launch the app. You’ll see the ghost’s body:
A ghost with a body? Only in Pacman. :]
Drawing the Ghost’s Head
The ghost’s head is a half-circle arc, but bigger and in the opposite direction of the ghost’s feet. Add the following code at the bottom of drawGhost
:
scope.drawArc(
Color.Red,
startAngle = 180f,
sweepAngle = 180f,
useCenter = false,
topLeft = Offset(ghostXPos - 150.dp.value, ghostYPos + 50.dp.value),
size = Size(150.dp.value, 150.dp.value)
)
Starting at the top left corner of the ghost’s body, you draw an arc 180 degrees to the right. Rebuild the project and run the app. You’ll see the ghost’s head:
Drawing the Ghost’s Eyes
Wow, all you’re missing now are the eyes! The ghost has two eyes, with each eye composed of a white outer circle and a black inner circle for the iris. So now, you’ll draw four circles just like you’ve already done with the power pellet. Add the following code at the bottom of drawGhost
:
scope.drawCircle(
Color.White,
center = Offset(ghostXPos - 100.dp.value, ghostYPos + 100.dp.value),
radius = 20f
)
scope.drawCircle(
Color.Black,
center = Offset(ghostXPos - 90.dp.value, ghostYPos + 100.dp.value),
radius = 10f
)
Rebuild the project and run the app. You’ll see a one-eyed ghost:
Now, try to draw the ghost’s left eye. Gotta have two eyes to catch Pacman. :]
Drawing Text With Jetpack Compose
To draw text, you need to access the native Canvas object because you can’t draw text on top of Jetpack Compose’s Canvas. Inside drawScore
, you’ll see that you have textPaint
:
val textPaint = Paint().asFrameworkPaint().apply {
isAntiAlias = true
textSize = 80.sp.value
color = android.graphics.Color.WHITE
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD)
textAlign = android.graphics.Paint.Align.CENTER
}
Text is drawn as a custom graphic using Jetpack Compose with Paint. You change the style and color of the text through this interface. Normally with Canvas, you use the base Paint object, but because you’re using the native Canvas object, you need the framework Paint method called asFrameworkPaint
. Inside the asFrameworkPaint.apply
block above, you configure the Paint object’s text for font, style, size and color.
Additionally, there’s no drawText
inside the DrawingScope
for the normal Canvas object. You need to call into the nativeCanvas
interface to access its drawText
method. To draw text on the native Canvas, add the following code below // 7. Draw a text
:
scope.drawIntoCanvas {
it.nativeCanvas.drawText( // 1
"HIGH SCORE", // 2
canvasWidth / 2, // 3
canvasHeight / 3, // 3
textPaint // 4
)
it.nativeCanvas.drawText( // 1
"360", // 2
canvasWidth / 2, // 3
canvasHeight / 3 + 100.dp.value, // 3
textPaint // 4
)
}
Here’s what’s happening:
- Like the Path in
arcTo
earlier,it
is the Canvas object inside thedrawIntoCanvas
block. You reference the native Canvas object and then usedrawText
to draw the text. - This is the text you want to write.
- These are the x and y coordinates for text placement.
- This is the framework Paint object representing the text’s color, size and font.
Build the project and run the app. You’ll see the following text on the screen:
Scaling, Translating and Rotating Objects With Jetpack Compose
You’ve done a lot of drawing! Sometimes, after drawing objects, you might need to transform them. For example, you might want to make them bigger or change their position or direction. You may also need to rotate Pacman as he moves through the maze: When he moves north, his mouth should point north.
Scaling and Translating Objects
Look at the ghost, and you’ll see he’s smaller than Pacman. Also, the ghost’s position is slightly lower than Pacman’s. You can fix this by transforming the ghost, which you’ll do now.
DrawingScope
has the withTransform
method. Inside this method, add the scale
and translate
modifiers. Inside drawGhost
wrap every code there with the following snippet below:
scope.withTransform({ // 1
scale(1.2f) // 2
translate(top=-50.dp.value, left=50.dp.value) // 3
}) {
...
}
Here’s what this code does:
- A
scope
block uses the methodwithTransform
. Inside the block,withTransform
uses two methods, or modifiers, to transform the object. -
scale
changes the size of the object. The argument1.2f
means the object will be 20% bigger. -
translate
has two arguments,top
andleft
.top
changes the vertical position of the object. The negative value,-50.dp.value
, means the object rises upward. A positive value pushes the object downward. The horizontal position of the object changes with theleft
argument. The negative value,-50.dp.value
, means object moves to the left. A positive value would move the object to the right.
Build the project and run the app. You’ll see the ghost has moved slightly up and become bigger:
Well, look at that! A Blinky replica. Blinky would be proud. Waka do you think? :]
Rotating Objects
The Pacman game has a lives indicator, and adding one will make this complete. The indicator is the Pacman shape itself. So, if the indicator has three Pacman shapes, it means you have three lives. You already know how to draw Pacman. Now, you need to duplicate him. But that’s not enough. You also need to move the clones, make them smaller, and then rotate them.
Go into drawPacmanLives
, and add the following code:
scope.withTransform({
scale(0.4f)
translate(top=1200.dp.value, left=-1050.dp.value)
rotate(180f)
}) {
drawArc(
Color.Yellow,
45f,
270f,
true,
pacmanOffset,
Size(200.dp.value, 200.dp.value)
)
}
You’ve seen the drawArc
code before. It’s the code you used when you drew the original Pacman. withTransform
will scale, translate, and finally, rotate Pacman. rotate
accepts the angle argument in degrees.
Build the project and run the app. You’ll see another Pacman but smaller and in a different place:
Now, try to draw another Pacman beside this clone to indicate that you have two lives left in the game.
Where to Go From Here?
Download the final project by clicking Download Materials at the top or bottom of this tutorial.
You’ve learned about the Graphics API in Jetpack Compose. You learned how to set up Canvas and modify it using a modifier. You’ve drawn many shapes and transformed them. You also used the native Canvas to draw text on it.
But there are more things you haven’t explored, such as animation. You could move Pacman and make his mouth open to simulate eating the dots. Of course, you could give life to the ghost, make it flash blue, and have it chase Pacman.
Feel free to checkout this wonderful Getting Started with Jetpack Compose Animations tutorial followed by the more advanced Jetpack Compose Animations tutorial.
We hope you enjoyed this tutorial! If you have any comments or questions, please join the forum discussion below.
[ad_2]
Source link