Make a Ticket View with Jetpack Compose

Kush Saini
4 min readNov 9, 2023

I wanted to create this ticket or receipt view using Compose, to compare the complexity with creating it with Custom Views using canvas, turns out it is quite similar as we have to deal with Path to create this, but there is one thing that is very easier in compose is to create a shadow, as I can remember creating shadows for custom view like these was complex or it was not easier to understand it if you don’t know what the methods are for. With Jetpack Compose creating shadows for was quite easy. So, let’s have a look at the code:

class TicketShape(
private val teethWidthDp: Float,
private val teethHeightDp: Float
) : Shape {

override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
) = Outline.Generic(Path().apply {

... // path drawing logic here

}

For creating a TicketView with Jetpack Compose, we implement Shape interface and override it’s method createOutline(…) which retrun a Outline object which we will define using AndroidPath class.

We can call a function Path() to get a instance of AndroidPath and create our shape with it.

Here is the logic to create the ticket shape:

I am using percentages to generalize the dimensions without hardcoding them, so the output shape can wrap any size.

Let’s define some variables we will be using to create the shape, they are pretty self-explanatory :

val teethHeightPx = teethHeightDp * density.density
var fullTeethWidthPx = teethWidthDp * density.density
var halfTeethWidthPx = fullTeethWidthPx / 2
val shapeWidthPx = size.width * 0.99f - size.width * 0.01f

var currentDrawPositionX = size.width * 0.99f // current X position of our pointer
var teethBasePositionY = size.height * 0.01f + teethHeightPx // position Y marking base of our teeth

First we move our drawing pointer to top-right corner of our shape

moveTo(
size.width * 0.99f,
size.height * 0.01f
)

Next, we have logic to find optimized count of teethes to fit the available width without overflowing or underflowing teethes, by modifying the teeth width.


val teethCount = shapeWidthPx / fullTeethWidthPx
val minTeethCount = floor(teethCount)

val newTeethWidthPx = shapeWidthPx / minTeethCount
fullTeethWidthPx = newTeethWidthPx
halfTeethWidthPx = fullTeethWidthPx / 2

We will draw half teeth at starting and half teeth at end of the top edge, to keep the drawn teeth count checked, we are starting with:

var drawnTeethCount = 1

Let’s draw the out top edge, we are drawing from top-right to top-left:

// draw half of first teeth
lineTo(
currentDrawPositionX - halfTeethWidthPx,
teethBasePositionY + teethHeightPx
)

// draw remaining teethes
while (drawnTeethCount < minTeethCount) {

currentDrawPositionX -= halfTeethWidthPx

// draw right half of teeth
lineTo(
currentDrawPositionX - halfTeethWidthPx,
teethBasePositionY - teethHeightPx
)

currentDrawPositionX -= halfTeethWidthPx

// draw left half of teeth
lineTo(
currentDrawPositionX - halfTeethWidthPx,
teethBasePositionY + teethHeightPx
)

drawnTeethCount++
}

currentDrawPositionX -= halfTeethWidthPx

// draw half of last teeth
lineTo(
currentDrawPositionX - halfTeethWidthPx,
teethBasePositionY - teethHeightPx
)

We are creating teethes by drawing a line moving right to left posting in x-axis, so, we are decreasing the currentDrawPositionX by half of teeth width, we are creating two halves of teeth, manipulating the Y position of our pointer by incrementing or decrementing teeth height from teethBasePositionY.

Next we will draw the left edge:

lineTo(
size.width * 0.01f,
size.height * 0.99f
)

Reset variable to draw bottom edge of teethes from left to right:

drawnTeethCount = 1
teethBasePositionY = size.height * 0.99f - teethHeightPx
currentDrawPositionX = size.width * 0.01f

Again, we will write similar logic to make the teethes in downward direction, from bottom-left to bottom-right:

// draw half of first teeth
lineTo(
currentDrawPositionX,
teethBasePositionY + teethHeightPx
)

lineTo(
currentDrawPositionX + halfTeethWidthPx,
teethBasePositionY - teethHeightPx
)

// draw remaining teethes
while (drawnTeethCount < minTeethCount) {

currentDrawPositionX += halfTeethWidthPx

// draw left half of teeth
lineTo(
currentDrawPositionX + halfTeethWidthPx,
teethBasePositionY + teethHeightPx
)

currentDrawPositionX += halfTeethWidthPx

// draw right half of teeth
lineTo(
currentDrawPositionX + halfTeethWidthPx,
teethBasePositionY - teethHeightPx
)

drawnTeethCount++
}

currentDrawPositionX += halfTeethWidthPx

// draw half of last teeth
lineTo(
currentDrawPositionX + halfTeethWidthPx,
teethBasePositionY + teethHeightPx
)

close() // close the path

Last thing, to add shadow we can use the generated Shape object from the TicketShape class to define a shape with shadow extension function of Modifier class.

Box(
modifier = Modifier
.padding(8.dp)
.shadow(
2.dp, // change the shadow as needed
shape = TicketShape(8f, 4f), // here
clip = true
)
.background(Color.White)
) {

Box(modifier = Modifier.padding(16.dp)) {
content()
}
}

Finally this is the result:

with 24dp teeth width and 8dp teeth height

You can find the whole code here:

--

--