Posted on Aug 17, 2023 • 15 min read
In this tutorial, we will explore how to create an HourGlass animation using Jetpack Compose. Jetpack Compose is a modern UI toolkit for building native Android interfaces using a declarative syntax. Our HourGlass animation will combine various Compose elements to simulate the classic Hourglass timer effect.
One of the most fascinating aspects of creating the HourGlass animation is the use of the Canvas Composable. Jetpack Compose provides the Canvas
Composable, which allows us to draw custom graphics and shapes directly onto the screen. In our HourGlass animation, we take full advantage of the capabilities offered by Canvas to bring our hourglass to life.
To begin, let’s create a Composable function called HourGlass that takes in a HourGlassViewModel and a HourGlassConfig. The HourGlassViewModel will provide us with the necessary data for our animation, while the HourGlassConfig will define the size and other configuration parameters of our hourglass.
@Composable
fun HourGlass(
hourGlassViewModel: HourGlassViewModel,
hourGlassConfig: HourGlassConfig,
) {
// Collect the current tick value from the ViewModel
val tick: Float by hourGlassViewModel.getTick.collectAsStateWithLifecycle(0f)
// Create a Box to constrain the animation within a specific size
BoxWithConstraints(
modifier = Modifier.size(hourGlassConfig.size),
) {
val center = Offset(
maxWidth.toPx() / 2,
maxHeight.toPx() / 2
)
// Draw the hourglass components using Canvas
Canvas(
modifier = Modifier,
) {
topGlass(center, hourGlassConfig)
bottomGlass(center, hourGlassConfig)
topSand(center, hourGlassConfig, tick)
bottomSand(center, hourGlassConfig, tick)
movingSand(tick, center, hourGlassConfig)
movingSandParticles(tick, center, hourGlassConfig)
}
}
}
Let’s briefly explain each step of the animation:
topGlass and bottomGlass: These functions draw the top and bottom parts of the hourglass frame.
topSand and bottomSand: These functions simulate the sand falling from the top to the bottom half of the hourglass based on the current tick value.
movingSand: This function draws the continuous flow of sand within the hourglass, creating the illusion of time passing.
movingSandParticles: Here, we add extra visual flair by creating small sand particles that move along with the falling sand.
data class HourGlassConfig(
val size: Dp,
val glassWidth: Dp = 2.dp,
val glassColor: Color = Color(0xFF966F33),
val glassGapForSand: Dp = 10.dp,
val sandColor: Color = Color(0xFFC2B280),
val sandWidth: Dp = 2.5.dp,
)
The HourGlassConfig data class encapsulates configuration parameters for an HourGlass animation.
It includes properties such as size (hourglass size), glassWidth (frame width), glassColor (frame color), glassGapForSand (a gap between glass halves), sandColor
(sand color), and sandWidth (sand particle width). By adjusting these properties, you can easily customize the appearance of the animation to create unique hourglass effects.
import android.os.CountDownTimer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
class HourGlassViewModel : ViewModel() {
// Represents the ongoing CountDownTimer instance
private var countDownTimer: CountDownTimer? = null
// A MutableSharedFlow that emits tick values representing animation progress
private val tick = MutableSharedFlow<Float>()
// Exposes the tick flow for observing animation progress
val getTick = tick
// Starts the countdown timer with the given duration in seconds
fun startTimer(durationInSecond: Long) {
// If a timer is already running, do nothing
if (countDownTimer != null) {
return
}
// Convert duration to milliseconds
val duration = durationInSecond * 1000L
// Create and start the CountDownTimer
countDownTimer = object : CountDownTimer(
duration,
1
) {
// This function is called on every tick (every millisecond)
override fun onTick(millisUntilFinished: Long) {
// Calculate animation progress and emit it using coroutine
viewModelScope.launch {
tick.emit((duration - millisUntilFinished).toFloat() / duration)
}
}
// This function is called when the timer finishes
override fun onFinish() {
// Emit a final progress value of 1.0 (100%)
viewModelScope.launch {
tick.emit(1f)
}
}
}.start()
}
// Stops the timer and resets the progress to 0
fun stopTimer() {
// Emit a progress value of 0 to reset animation
viewModelScope.launch {
tick.emit(0f)
}
// Cancel the ongoing timer, if any
countDownTimer?.cancel()
// Set the timer instance to null
countDownTimer = null
}
}
In this code:
We use a CountDownTimer to manage the animation timing, emitting progress ticks.
The tick flow is a shared flow that emits progress values.
startTimer initializes the timer and emits progress values as time passes.
stopTimer cancels the timer and resets the progress to 0.
This ViewModel handles the countdown logic, ensuring that animation progress is accurately tracked and emitted for your HourGlass animation.
// This function draws the top glass frame of the hourglass.
fun DrawScope.topGlass(
center: Offset,
hourGlassConfig: HourGlassConfig,
) {
with(hourGlassConfig) {
val length = center.x
// Create a Path to define the shape of the top glass frame
drawPath(
path = Path().apply {
moveTo(center.x - length, center.y - length)
lineTo(center.x + length, center.y - length)
lineTo(center.x + glassGapForSand.value / 2, center.y)
moveTo(center.x - glassGapForSand.value / 2, center.y)
lineTo(center.x - length, center.y - length)
},
color = glassColor,
style = Stroke(width = glassWidth.value)
)
}
}
The topGlass the function draws the top part of the hourglass frame. It creates a Path object using the drawPath function.
// This function draws the bottom glass frame of the hourglass.
fun DrawScope.bottomGlass(
center: Offset,
hourGlassConfig: HourGlassConfig,
) {
// Rotate the canvas by 180 degrees to draw the bottom glass
rotate(
degrees = 180f,
pivot = center
) {
// Call the topGlass function to draw the bottom glass
topGlass(center, hourGlassConfig)
}
}
This function uses the rotate function to flip the canvas by 180 degrees around the center point. This is done to draw the bottom glass frame of the hourglass. Within the rotated canvas, it calls the topGlass function to draw the bottom glass frame. This simplifies the code by reusing the same drawing logic for both the top and bottom glass.
// This function draws the sand in the top half of the hourglass.
fun DrawScope.topSand(
center: Offset,
hourGlassConfig: HourGlassConfig,
tick: Float = 0f,
) {
with(hourGlassConfig) {
val length = center.x
var variableLength = length - glassWidth.value / 2
variableLength -= variableLength * tick
// Create a Path to define the shape of the sand in the top half
drawPath(
path = Path().apply {
moveTo(center.x - variableLength, center.y - variableLength)
lineTo(center.x + variableLength, center.y - variableLength)
lineTo(center.x, center.y + glassWidth.value)
moveTo(center.x, center.y)
lineTo(center.x - variableLength, center.y - variableLength)
},
color = sandColor,
)
}
}
The topSand function draws the sand in the top half of the hourglass. It calculates the variableLength of the sand based on the animation progress (tick). This variableLength decreases as the animation progresses, giving the illusion of sand moving down from the top half.
The drawPath function is used to create a triangular shape for the sand using moveTo and lineTo functions. The sand color is set from hourGlassConfig.
// This function draws the sand in the bottom half of the hourglass.
fun DrawScope.bottomSand(
center: Offset,
hourGlassConfig: HourGlassConfig,
tick: Float,
) {
with(hourGlassConfig) {
val clipLength = center.x - glassWidth.value / 2
// Clip the canvas to draw sand only within the bottom glass
clipPath(
path = Path().apply {
moveTo(center.x + clipLength, center.y + clipLength)
lineTo(center.x - clipLength, center.y + clipLength)
lineTo(center.x, center.y - glassWidth.value)
moveTo(center.x, center.y)
lineTo(center.x + clipLength, center.y + clipLength)
},
clipOp = ClipOp.Intersect
) {
val rectLength = center.x * (1f - tick)
// Draw a rectangle to represent the sand in the bottom half
filledRect(center, center.x, hourGlassConfig.sandColor)
// Draw a white rectangle to cover the sand above the current level
filledRect(center, rectLength, Color.White)
}
}
}
// This function draws a filled rectangle at a specified position.
fun DrawScope.filledRect(
center: Offset,
rectLength: Float,
color: Color,
) {
// Create a Path to define the shape of the filled rectangle
drawPath(
path = Path().apply {
moveTo(center.x + rectLength, center.y + rectLength)
lineTo(center.x - rectLength, center.y + rectLength)
lineTo(center.x + rectLength, center.y)
moveTo(center.x + rectLength, center.y)
lineTo(center.x - rectLength, center.y)
lineTo(center.x - rectLength, center.y + rectLength)
},
brush = SolidColor(color),
)
}
The bottomSand function draws the sand in the bottom half of the hourglass. It uses the clipPath function to restrict the drawing area to the bottom glass section. Within the clipped area, it first draws a rectangle representing the sand using the filledRect function. Then, it draws a white rectangle above the current sand level to simulate the disappearing sand effect as it moves to the bottom.
// This function draws the continuous flow of sand in the hourglass.
fun DrawScope.movingSand(
tick: Float,
center: Offset,
hourGlassConfig: HourGlassConfig,
) {
if (tick > 0f) {
// Draw a line representing the falling sand
drawLine(
color = Color(0xFFC2B280),
start = center,
end = Offset(center.x, center.y + center.x - (hourGlassConfig.glassWidth.value / 2)),
strokeWidth = hourGlassConfig.sandWidth.toPx()
)
}
}
This function draws the continuous flow of sand particles in the hourglass. If the tick value is greater than 0, it draws a vertical line using the drawLine function to represent the falling sand. The line starts from the center of the hourglass and extends down to the current sand level. The sandWidth from hourGlassConfig is used to set the line's width.
// This function draws moving sand particles in the hourglass.
fun DrawScope.movingSandParticles(
tick: Float,
center: Offset,
hourGlassConfig: HourGlassConfig,
) {
// Check if time is moving (tick > 0)
if (tick > 0f) {
// Draw multiple sand particles using random offsets
for (i in 0..10) {
// Generate a random position for the sand particle
val random = Random.nextInt(center.y.dp.toPx().toInt())
// Draw the sand particle using the drawPoints function
drawPoints(
pointMode = PointMode.Points,
points = listOf(
Offset(center.x - 1.dp.toPx(), min(center.y + random.toFloat(), center.y + center.x) - hourGlassConfig.glassWidth.value * 2),
Offset(center.x + 1.dp.toPx(), min(center.y + random.toFloat(), center.y + center.x) - hourGlassConfig.glassWidth.value * 2),
),
color = hourGlassConfig.sandColor,
strokeWidth = 4f
)
}
}
}
Inside the loop (a repeating action), the code does the following 10 times:
It uses a special tool called Random to pick a random number. This number helps decide where to put each sand particle inside the hourglass.
Then, the code uses the drawPoints function to draw the sand particles. It does this by giving it some instructions:
pointMode = PointMode.Points says that we're drawing individual points, like tiny dots. points = listOf(...) gives a list of positions for the sand particles. Each particle is drawn using two points.
These points are calculated based on the center of the hourglass, and they are slightly different for each particle. This makes the particles look like they're moving around randomly.
The color of the sand particles is set to the color of sand from the hourGlassConfig.
The strokeWidth determines how thick each sand particle should be drawn.
And that’s how this code creates the effect of moving sand particles inside the hourglass. It’s like adding a bunch of tiny dots that move around to show the sand falling and swirling inside the timer.
HourGlass(
hourGlassViewModel,
hourGlassConfig = HourGlassConfig(
size = 100.dp,
)
)
Pass the ViewModel and the config, and To start the glass,
// Duration is Seconds
hourGlassViewModel.startTimer(3600)
Tech Holding Team is a AWS Certified & validates cloud expertise to help professionals highlight in-demand skills and organizations build effective, innovative teams for cloud initiatives using AWS.
By using this site, you agree to thePrivacy Policy.