Hey there! Welcome to my blog on creating hourglass animation with drawing on canvas in android with jetpack compose. Let’s get started.
Make sure you have below dependencies with latest version. And I’m using version 1.2.0-beta02 as of now.
You can take reference of the source code from github repository.
compose_version = '1.2.0-beta02'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
1. Setup theme palette and colors
Let’s setup app theme and palette colors before adding shapes to canvas. This is help you achieve same exact design like the one in preview image.
val white = Color(0xFFffffff)
val grey100 = Color(0xFFf5f5f5)
val grey300 = Color(0xFFe0e0e0)
val grey500 = Color(0xFF9e9e9e)
val grey700 = Color(0xFF616161)
val grey900 = Color(0xFF212121)
val grey = Color(0xFF181818)
val black = Color(0xFF000000)
private val DarkColorPalette = darkColors(
primary = grey500,
primaryVariant = grey700,
secondary = grey500,
secondaryVariant = grey300,
background = black,
onBackground = grey100,
surface = grey
)
private val LightColorPalette = lightColors(
primary = grey700,
primaryVariant = grey500,
secondary = grey700,
secondaryVariant = grey900,
background = white,
onBackground = grey900,
surface = grey100
)
All colors will depend on below white to dark grey variants for both light/dark colors.
@Composable
fun ComposeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
content = content
)
}
2. Draw shapes on canvas
Above hourglass is drawn in canvas with two shapes i.e., 2 rectangles and 8 lines.
1. Rounded corner rectangle without filling
2. Lines drawn using start and end of x , y offsets
Draw plane rectangle on canvas with specific width and height
Canvas(
modifier = Modifier.size(300.dp, 180.dp)
) {
val canvasSize = size
val canvasWidth = size.width
val canvasHeight = size.height
drawRoundRect(
size = canvasSize / 2F,
color = grey900,
topLeft = Offset(
x = canvasWidth / 4F,
y = canvasHeight / 3F
)
)
}
Draw rectangle with rounded corners and only borders
Canvas(
modifier = Modifier.size(300.dp, 180.dp)
) {
val canvasSize = size
val canvasWidth = size.width
val canvasHeight = size.height
drawRoundRect(
size = canvasSize / 2F,
cornerRadius = CornerRadius(60F, 60F),
color = grey900,
topLeft = Offset(
x = canvasWidth / 4F,
y = canvasHeight / 3F
),
style = Stroke(width = 16F)
)
}
We will make use of same rectangle which added above in hourglass component.
Draw horizontal line of stroke width 16f on canvas with specific start and end offsets
Canvas(
modifier = Modifier.fillMaxSize()
) {
drawLine(
start = Offset(x = 200f, y = 200f),
end = Offset(x = 800f, y = 200f),
color = grey900,
strokeWidth = 16F
)
}
If you need rounded corners for lines you have to add one more property to drawLine.
drawScope.drawLine( ... cap = StrokeCap.Round )
Now we know how to draw lines and rectangles, that’s all we need to build hourglass now.
3. Build hourglass on canvas
Let’s divide component into four parts and let’s add them in single column.

1. Top rectangle
2. Left side lines
3. Right side lines
4. Bottom rectangle
Let’s add Column layout as parent so that we can arrange shapes in vertical orientation.
Let canvas take whole space along with that arrange items vertically center and align horizontally center. Doing this child components inside column won’t overlap and aligned center perfectly.
@Composable
fun Hourglass() {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
DrawRoundRectangle()
DrawLines()
DrawRoundRectangle()
}
}
Now add those composable functions from below.
Rounded Rectangle
@Composable
fun DrawRoundRectangle() {
Canvas(
modifier = Modifier.fillMaxWidth().height(60.dp)
) {
val canvasSize = size
val canvasWidth = size.width
val canvasHeight = size.height
drawRoundRect(
size = canvasSize / 2F,
cornerRadius = CornerRadius(60F, 60F),
color = grey900,
style = Stroke(width = 16F),
topLeft = Offset(
x = canvasWidth / 4F,
y = canvasHeight / 3F
)
)
}
}
Lines: StrokeCap rounded
@Composable
fun DrawLines() {
Canvas(
modifier = Modifier.fillMaxWidth().height(180.dp)
) {
// Left top
drawSingleLine(340f, -20f, 340f, 140f, this)
// Left top diagonal
drawSingleLine(340f, 140f, 460f, 260f, this)
// Left bottom diagonal
drawSingleLine(460f, 260f, 340f, 380f, this)
// Left bottom
drawSingleLine(340f, 380f, 340f, 540f, this)
// Right bottom
drawSingleLine(740f, 380f, 740f, 540f, this)
// Right top diagonal
drawSingleLine(740f, 140f, 620f, 260f, this)
// Right bottom diagonal
drawSingleLine(620f, 260f, 740f, 380f, this)
// Right top
drawSingleLine(740f, -20f, 740f, 140f, this)
}
}
Above you have called same function drawSingleLine( ) with 5 arguments.
Here’s what each value in argument represents.
- startX – First point of the line to be drawn in X
- startY – First point of the line to be drawn in Y
- endX – End point of the line to be drawn in X
- endY – End point of the line to be drawn in Y
- this – We can draw only inside the DrawScope
Now let’s create that reusable function which draws lines for us.
fun drawSingleLine(
startX: Float,
startY: Float,
endX: Float,
endY: Float,
drawScope: DrawScope
) {
drawScope.drawLine(
start = Offset(startX, startY),
end = Offset(endX, endY),
color = grey900,
strokeWidth = 16F,
cap = StrokeCap.Round
)
}
At this point you must notice Hourglass drawn on canvas perfectly. I have kept width and height based on my convenience, if it’s not working for you or preview isn’t as expected, then you need to make changes to those.
4. Animate shape and color
Let’s animate changes from low to high scale of hourglass shape along with color.

Notice in left side the color is lighter gray than compared to right side. With increase in duration brighter it becomes. For this we need to apply animateColor( ) with required arguments.
Animate color changes
@Composable
fun colorShapeTransition(
initialValue: Color,
targetValue: Color,
durationMillis: Int
): Color {
val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
initialValue = initialValue,
targetValue = targetValue,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Restart
)
)
return color
}
- intialColor – Color initially applied when canvas lays the object.
- targetColor – Final color applied at the end of the duration.
- durationMillis – Amount of time taken by object to reach final state visibility.
Finally call infiniteTransition since we would like to animate this infinitely and if you want to save the state of animation at any point with rememberInfiniteTransition( )
Animate scale changes
Apply same terminology as above. This time we apply scale from 0.1f initial value to 1f target with applied duration.
@Composable
fun scaleShapeTransition(
initialValue: Float,
targetValue: Float,
durationMillis: Int
): Float {
val infiniteTransition = rememberInfiniteTransition()
val scale: Float by infiniteTransition.animateFloat(
initialValue = initialValue,
targetValue = targetValue,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis),
repeatMode = RepeatMode.Restart
)
)
return scale
}
Now call this both functions to our hourglass modifiers graphicsLayer like below.
Below is the final state of Hourglass object after applying all changes.
@Composable
fun Hourglass() {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val scale = scaleShapeTransition(0.1f, 1f, 2000)
val lineColor = colorShapeTransition(grey900, grey100, 2000)
DrawRoundRectangle(scale, lineColor)
DrawLines(scale, lineColor)
DrawRoundRectangle(scale, lineColor)
}
}
@Composable
fun DrawLines(
scale: Float,
lineColor: Color
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
.graphicsLayer {
scaleX = scale
scaleY = scale
}
) {
// Left top
drawSingleLine(340f, -20f, 340f, 140f, lineColor, this)
// Left top diagonal
drawSingleLine(340f, 140f, 460f, 260f, lineColor, this)
// Left bottom diagonal
drawSingleLine(460f, 260f, 340f, 380f, lineColor, this)
// Left bottom
drawSingleLine(340f, 380f, 340f, 540f, lineColor, this)
// Right bottom
drawSingleLine(740f, 380f, 740f, 540f, lineColor, this)
// Right top diagonal
drawSingleLine(740f, 140f, 620f, 260f, lineColor, this)
// Right bottom diagonal
drawSingleLine(620f, 260f, 740f, 380f, lineColor, this)
// Right top
drawSingleLine(740f, -20f, 740f, 140f, lineColor, this)
}
}
@Composable
fun DrawRoundRectangle(
scale: Float,
lineColor: Color
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.graphicsLayer {
scaleX = scale
scaleY = scale
}
) {
val canvasSize = size
val canvasWidth = size.width
val canvasHeight = size.height
drawRoundRect(
size = canvasSize / 2F,
cornerRadius = CornerRadius(60F, 60F),
color = lineColor,
style = Stroke(width = 16F),
topLeft = Offset(
x = canvasWidth / 4F,
y = canvasHeight / 3F
)
)
}
}
fun drawSingleLine(
startX: Float,
startY: Float,
endX: Float,
endY: Float,
lineColor: Color,
drawScope: DrawScope
) {
drawScope.drawLine(
start = Offset(startX, startY),
end = Offset(endX, endY),
color = lineColor,
strokeWidth = 16F,
cap = StrokeCap.Round
)
}
Now call composable function Hourglass anywhere you would like to show that object. If it is the only object you are left with you may directly link it to entry point of your activity like below.
5. Set entry point and topBar
Set background to whole layout with topBar and background. Let remaining body space occupied by hourglass.
@Composable
fun HourglassAnimation() {
Surface(
color = MaterialTheme.colors.background
) {
Scaffold(
topBar = {
Text(
text = "Hourglass Animation",
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.h5,
modifier = Modifier.padding(16.dp)
)
}
) {
Hourglass()
}
}
}
Call that HourglassAnimation composable function in activity.
@ExperimentalAnimationApi
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTheme {
HourglassAnimation()
}
}
}
}
That’s it guys, thanks for reading and do check my GitHub from below you find more useful repositories.
6. Project and GitHub resources
Android Documentation on getting started with canvas

Rajasekhar K E
Hi ! I’m Rajasekhar a Programmer who does Android Development, Creative & Technical writing, Kotlin enthusiast and Engineering graduate. I learn from Open Source and always happy to assist others with my work. I spend most of time Training, Assisting & Mentoring students who are absolute Beginners in android development. I’m also running my startup named Developers Breach which mostly works on contributing to open source.
Here We Go Again : (
if (article == helpful) {
println("Like and subscribe to blog newsletter.")
} else {
println("Let me know what i should blog on.")
}
You must be logged in to post a comment.