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.0.2 as of now.

compose_version = '1.0.2'

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.

  1. startX – First point of the line to be drawn in X
  2. startY – First point of the line to be drawn in Y
  3. endX – End point of the line to be drawn in X
  4. endY – End point of the line to be drawn in Y
  5. 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
}
  1. intialColor – Color initially applied when canvas lays the object.
  2. targetColor – Final color applied at the end of the duration.
  3. 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

Become Author

We are making content on new declarative UI kit Jetpack Compose, if you are willing to build content join us and we will help you get started.

Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

This site uses Akismet to reduce spam. Learn how your comment data is processed.