Jetpack compose Space animation
Animating things in jetpack compose is quite simple; this is a short explanation of mimicking a space background.
I will divide this into several small steps
- Draw a point
- Animate the alpha for the point
- Put random stars on the sky
- Using different starting alpha values
Draw a point
For having some order on this, we are going to have a structure for helping us draw our “stars”:
data class Star(
var x: Float,
var y: Float,
var alpha: Float,
)
First of all, we need to use a Canvas
for achieving this. And drawing a point is relatively easy on a Canvas:
val star = Star(50f, 50f, 0.5f)
Canvas(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
) {
drawCircle(
color = Color.White,
center = Offset(star.x, star.y),
radius = 2f,
alpha = star.alpha,
)
}
In this case, we have our star object, and we can render it at our Container, so we need to animate things for the next step.
Animate the alpha value
This one needs a slight modification just for the alpha value.
val infiniteTransition = rememberInfiniteTransition()
val infinitelyAnimatedFloat = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(2000),
repeatMode = RepeatMode.Reverse
)
)
By creating an animation with values from 0 to 1, we can turn on/off the sky.
A bunch of stars
Now we need to create multiple random Star
objects and place them everywhere around the Canvas, for knowing that we have on our Container, we can use the BoxWithConstraints component.
BoxWithConstraints(
modifier = modifier
.fillMaxWidth()
.fillMaxHeight(),
) {
val width = maxWidth.toPx()
val height = maxHeight.toPx()
val stars = remember {
buildList {
repeat(1000) {
val x = (Math.random() * width).toFloat()
val y = (Math.random() * height).toFloat()
val alpha = Math.random().toFloat()
add(Star(x, y, alpha))
}
}
}
Canvas(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
) {
for (star in stars) {
drawCircle(
color = Color.White,
center = Offset(star.x, star.y),
radius = 2f,
alpha = infinitelyAnimatedFloat.value,
)
}
}
}
As you can imagine, this will give us a thousand random stars in our sky, and the only problem is that they are appearing/disappearing simultaneously.
Starting in a different alpha value
We will rely upon an old concept used in games in which we have an update function for the star values. The sin function will do the magic for transforming continuous values into something that grows up and down.
We need to modify this to operate with values based on Π, which will give valid end values for the sin function.
val infiniteTransition = rememberInfiniteTransition()
val infinitelyAnimatedFloat = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 2f * Math.PI.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(10000),
repeatMode = RepeatMode.Restart
)
)
Our values will be given by these infinite values, from 0 to 2Π.
A modification to the Star data class is needed to update the values:
data class Star(
var x: Float,
var y: Float,
var alpha: Float,
) {
private val initialAlpha = alpha
fun update(value: Float) {
// ... update our values
}
}
with some changes on the sin function to get only positive numbers from 0 to 1, we can get this.
1/2 + ( 1/2 + sin(value — initialAlpha))
So our update function will look like this.
fun update(value: Float) {
val x = (value - initialAlpha).toDouble()
val newAlpha = 0.5f + (0.5f * Math.sin(x).toFloat())
alpha = newAlpha
}
Now we can deal with thousands of items on the same canvas.
All the code will look like this:
data class Star(
var x: Float,
var y: Float,
var alpha: Float,
) {
private val initialAlpha = alpha
fun update(value: Float) {
val x = (value - initialAlpha).toDouble()
val newAlpha = 0.5f + (0.5f * Math.sin(x).toFloat())
alpha = newAlpha
}
}
@OptIn(ExperimentalStdlibApi::class)
@Composable
fun Space(
modifier: Modifier = Modifier
) {
val infiniteTransition = rememberInfiniteTransition()
val infinitelyAnimatedFloat = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 2f * Math.PI.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(10000),
repeatMode = RepeatMode.Restart
)
)
BoxWithConstraints(
modifier = modifier
.fillMaxWidth()
.fillMaxHeight(),
) {
val width = maxWidth.toPx()
val height = maxHeight.toPx()
val stars = remember {
buildList {
repeat(1000) {
val x = (Math.random() * width).toFloat()
val y = (Math.random() * height).toFloat()
val alpha = (Math.random() * 2.0 * Math.PI).toFloat()
add(Star(x, y, alpha))
}
}
}
Canvas(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
) {
for (star in stars) {
star.update(infinitelyAnimatedFloat.value)
drawCircle(
color = Color.White,
center = Offset(star.x, star.y),
radius = 2f,
alpha = star.alpha,
)
}
}
}
}
The Result
In this image, we can see the result, our points turning on/off on the background of our app.