Skip to content

Instantly share code, notes, and snippets.

@EugeneTheDev
Created March 18, 2021 23:15
Show Gist options
  • Star 49 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save EugeneTheDev/a27664cb7e7899f964348b05883cbccd to your computer and use it in GitHub Desktop.
Save EugeneTheDev/a27664cb7e7899f964348b05883cbccd to your computer and use it in GitHub Desktop.
Dots loading animations with Jetpack Compose
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
val dotSize = 24.dp // made it bigger for demo
val delayUnit = 300 // you can change delay to change animation speed
@Composable
fun DotsPulsing() {
@Composable
fun Dot(
scale: Float
) = Spacer(
Modifier
.size(dotSize)
.scale(scale)
.background(
color = MaterialTheme.colors.primary,
shape = CircleShape
)
)
val infiniteTransition = rememberInfiniteTransition()
@Composable
fun animateScaleWithDelay(delay: Int) = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = delayUnit * 4
0f at delay with LinearEasing
1f at delay + delayUnit with LinearEasing
0f at delay + delayUnit * 2
}
)
)
val scale1 by animateScaleWithDelay(0)
val scale2 by animateScaleWithDelay(delayUnit)
val scale3 by animateScaleWithDelay(delayUnit * 2)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
val spaceSize = 2.dp
Dot(scale1)
Spacer(Modifier.width(spaceSize))
Dot(scale2)
Spacer(Modifier.width(spaceSize))
Dot(scale3)
}
}
@Composable
fun DotsElastic() {
val minScale = 0.6f
@Composable
fun Dot(
scale: Float
) = Spacer(
Modifier
.size(dotSize)
.scale(scaleX = minScale, scaleY = scale)
.background(
color = MaterialTheme.colors.primary,
shape = CircleShape
)
)
val infiniteTransition = rememberInfiniteTransition()
@Composable
fun animateScaleWithDelay(delay: Int) = infiniteTransition.animateFloat(
initialValue = minScale,
targetValue = minScale,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = delayUnit * 4
minScale at delay with LinearEasing
1f at delay + delayUnit with LinearEasing
minScale at delay + delayUnit * 2
}
)
)
val scale1 by animateScaleWithDelay(0)
val scale2 by animateScaleWithDelay(delayUnit)
val scale3 by animateScaleWithDelay(delayUnit * 2)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
val spaceSize = 2.dp
Dot(scale1)
Spacer(Modifier.width(spaceSize))
Dot(scale2)
Spacer(Modifier.width(spaceSize))
Dot(scale3)
}
}
@Composable
fun DotsFlashing() {
val minAlpha = 0.1f
@Composable
fun Dot(
alpha: Float
) = Spacer(
Modifier
.size(dotSize)
.alpha(alpha)
.background(
color = MaterialTheme.colors.primary,
shape = CircleShape
)
)
val infiniteTransition = rememberInfiniteTransition()
@Composable
fun animateAlphaWithDelay(delay: Int) = infiniteTransition.animateFloat(
initialValue = minAlpha,
targetValue = minAlpha,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = delayUnit * 4
minAlpha at delay with LinearEasing
1f at delay + delayUnit with LinearEasing
minAlpha at delay + delayUnit * 2
}
)
)
val alpha1 by animateAlphaWithDelay(0)
val alpha2 by animateAlphaWithDelay(delayUnit)
val alpha3 by animateAlphaWithDelay(delayUnit * 2)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
val spaceSize = 2.dp
Dot(alpha1)
Spacer(Modifier.width(spaceSize))
Dot(alpha2)
Spacer(Modifier.width(spaceSize))
Dot(alpha3)
}
}
@Composable
fun DotsTyping() {
val maxOffset = 10f
@Composable
fun Dot(
offset: Float
) = Spacer(
Modifier
.size(dotSize)
.offset(y = -offset.dp)
.background(
color = MaterialTheme.colors.primary,
shape = CircleShape
)
)
val infiniteTransition = rememberInfiniteTransition()
@Composable
fun animateOffsetWithDelay(delay: Int) = infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = delayUnit * 4
0f at delay with LinearEasing
maxOffset at delay + delayUnit with LinearEasing
0f at delay + delayUnit * 2
}
)
)
val offset1 by animateOffsetWithDelay(0)
val offset2 by animateOffsetWithDelay(delayUnit)
val offset3 by animateOffsetWithDelay(delayUnit * 2)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(top = maxOffset.dp)
) {
val spaceSize = 2.dp
Dot(offset1)
Spacer(Modifier.width(spaceSize))
Dot(offset2)
Spacer(Modifier.width(spaceSize))
Dot(offset3)
}
}
@Composable
fun DotsCollision() {
val maxOffset = 30f
val delayUnit = 500 // it's better to use longer delay for this animation
@Composable
fun Dot(
offset: Float
) = Spacer(
Modifier
.size(dotSize)
.offset(x = offset.dp)
.background(
color = MaterialTheme.colors.primary,
shape = CircleShape
)
)
val infiniteTransition = rememberInfiniteTransition()
val offsetLeft by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = delayUnit * 3
0f at 0 with LinearEasing
-maxOffset at delayUnit / 2 with LinearEasing
0f at delayUnit
}
)
)
val offsetRight by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = delayUnit * 3
0f at delayUnit with LinearEasing
maxOffset at delayUnit + delayUnit / 2 with LinearEasing
0f at delayUnit * 2
}
)
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = maxOffset.dp)
) {
val spaceSize = 2.dp
Dot(offsetLeft)
Spacer(Modifier.width(spaceSize))
Dot(0f)
Spacer(Modifier.width(spaceSize))
Dot(offsetRight)
}
}
@Preview(showBackground = true)
@Composable
fun DotsPreview() = MaterialTheme {
Column(modifier = Modifier.padding(4.dp)) {
val spaceSize = 16.dp
Text(
text = "Dots pulsing",
style = MaterialTheme.typography.h5
)
DotsPulsing()
Spacer(Modifier.height(spaceSize))
Text(
text = "Dots elastic",
style = MaterialTheme.typography.h5
)
DotsElastic()
Spacer(Modifier.height(spaceSize))
Text(
text = "Dots flashing",
style = MaterialTheme.typography.h5
)
DotsFlashing()
Spacer(Modifier.height(spaceSize))
Text(
text = "Dots typing",
style = MaterialTheme.typography.h5
)
DotsTyping()
Spacer(Modifier.height(spaceSize))
Text(
text = "Dots collision",
style = MaterialTheme.typography.h5
)
DotsCollision()
}
}
@mahfuznow
Copy link

mahfuznow commented Nov 29, 2022

Added support for modifying number of dots & it's color

import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

const val numberOfDots = 7
val dotSize = 10.dp
val dotColor: Color = Color.Blue
const val delayUnit = 200
const val duration = numberOfDots * delayUnit
val spaceBetween = 2.dp

@Composable
fun DotsPulsing() {

    @Composable
    fun Dot(scale: Float) {
        Spacer(
            Modifier
                .size(dotSize)
                .scale(scale)
                .background(
                    color = dotColor,
                    shape = CircleShape
                )
        )
    }

    val infiniteTransition = rememberInfiniteTransition()

    @Composable
    fun animateScaleWithDelay(delay: Int) = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 0f,
        animationSpec = infiniteRepeatable(animation = keyframes {
            durationMillis = delayUnit * numberOfDots
            0f at delay with LinearEasing
            1f at delay + delayUnit with LinearEasing
            0f at delay + duration
        })
    )

    val scales = arrayListOf<State<Float>>()
    for (i in 0 until numberOfDots) {
        scales.add(animateScaleWithDelay(delay = i * delayUnit))
    }

    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.Center
    ) {
        scales.forEach {
            Dot(it.value)
            Spacer(Modifier.width(spaceBetween))
        }
    }
}


@Composable
fun DotsElastic() {
    val minScale = 0.6f

    @Composable
    fun Dot(scale: Float) {
        Spacer(
            Modifier
                .size(dotSize)
                .scale(scaleX = minScale, scaleY = scale)
                .background(
                    color = dotColor,
                    shape = CircleShape
                )
        )
    }

    val infiniteTransition = rememberInfiniteTransition()

    @Composable
    fun animateScaleWithDelay(delay: Int) = infiniteTransition.animateFloat(
        initialValue = minScale,
        targetValue = minScale,
        animationSpec = infiniteRepeatable(animation = keyframes {
            durationMillis = duration
            minScale at delay with LinearEasing
            1f at delay + delayUnit with LinearEasing
            minScale at delay + duration
        })
    )

    val scales = arrayListOf<State<Float>>()
    for (i in 0 until numberOfDots) {
        scales.add(animateScaleWithDelay(delay = i * delayUnit))
    }

    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.Center
    ) {
        scales.forEach {
            Dot(it.value)
            Spacer(Modifier.width(spaceBetween))
        }
    }
}

@Composable
fun DotsFlashing() {
    val minAlpha = 0.1f

    @Composable
    fun Dot(alpha: Float) = Spacer(
        Modifier
            .size(dotSize)
            .alpha(alpha)
            .background(
                color = dotColor, shape = CircleShape
            )
    )

    val infiniteTransition = rememberInfiniteTransition()

    @Composable
    fun animateAlphaWithDelay(delay: Int) = infiniteTransition.animateFloat(
        initialValue = minAlpha,
        targetValue = minAlpha,
        animationSpec = infiniteRepeatable(animation = keyframes {
            durationMillis = duration
            minAlpha at delay with LinearEasing
            1f at delay + delayUnit with LinearEasing
            minAlpha at delay + duration
        })
    )

    val alphas = arrayListOf<State<Float>>()
    for (i in 0 until numberOfDots) {
        alphas.add(animateAlphaWithDelay(delay = i * delayUnit))
    }

    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.Center
    ) {
        alphas.forEach {
            Dot(it.value)
            Spacer(Modifier.width(spaceBetween))
        }
    }
}

@Composable
fun DotsTyping() {
    val maxOffset = (numberOfDots * 2).toFloat()

    @Composable
    fun Dot(offset: Float) {
        Spacer(
            Modifier
                .size(dotSize)
                .offset(y = -offset.dp)
                .background(
                    color = dotColor,
                    shape = CircleShape
                )
        )
    }

    val infiniteTransition = rememberInfiniteTransition()

    @Composable
    fun animateOffsetWithDelay(delay: Int) = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 0f,
        animationSpec = infiniteRepeatable(animation = keyframes {
            durationMillis = duration
            0f at delay with LinearEasing
            maxOffset at delay + delayUnit with LinearEasing
            0f at delay + (duration/2)
        })
    )

    val offsets = arrayListOf<State<Float>>()
    for (i in 0 until numberOfDots) {
        offsets.add(animateOffsetWithDelay(delay = i * delayUnit))
    }

    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.Center,
        modifier = Modifier.padding(top = maxOffset.dp)
    ) {
        offsets.forEach {
            Dot(it.value)
            Spacer(Modifier.width(spaceBetween))
        }
    }
}

@Composable
fun DotsCollision() {
    val maxOffset = 30f
    val delayUnit = 500

    @Composable
    fun Dot(offset: Float) {
        Spacer(
            Modifier
                .size(dotSize)
                .offset(x = offset.dp)
                .background(
                    color = dotColor,
                    shape = CircleShape
                )
        )
    }

    val infiniteTransition = rememberInfiniteTransition()

    val offsetLeft by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 0f,
        animationSpec = infiniteRepeatable(animation = keyframes {
            durationMillis = delayUnit * 3
            0f at 0 with LinearEasing
            -maxOffset at delayUnit / 2 with LinearEasing
            0f at delayUnit
        })
    )
    val offsetRight by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 0f,
        animationSpec = infiniteRepeatable(animation = keyframes {
            durationMillis = delayUnit * 3
            0f at delayUnit with LinearEasing
            maxOffset at delayUnit + delayUnit / 2 with LinearEasing
            0f at delayUnit * 2
        })
    )

    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.Center,
        modifier = Modifier.padding(horizontal = maxOffset.dp)
    ) {
        Dot(offsetLeft)
        Spacer(Modifier.width(spaceBetween))
        for (i in 0 until numberOfDots-2) {
            Dot(0f)
            Spacer(Modifier.width(spaceBetween))
        }
        Dot(offsetRight)
    }
}


@Preview(showBackground = true)
@Composable
fun DotsPreview() = MaterialTheme {
    Column(modifier = Modifier.padding(4.dp)) {
        val spaceSize = 16.dp

        Text(
            text = "Dots pulsing", style = MaterialTheme.typography.h5
        )
        DotsPulsing()

        Spacer(Modifier.height(spaceSize))

        Text(
            text = "Dots elastic", style = MaterialTheme.typography.h5
        )
        DotsElastic()

        Spacer(Modifier.height(spaceSize))

        Text(
            text = "Dots flashing", style = MaterialTheme.typography.h5
        )
        DotsFlashing()

        Spacer(Modifier.height(spaceSize))

        Text(
            text = "Dots typing", style = MaterialTheme.typography.h5
        )
        DotsTyping()

        Spacer(Modifier.height(spaceSize))

        Text(
            text = "Dots collision", style = MaterialTheme.typography.h5
        )
        DotsCollision()
    }
}

@razaghimahdi
Copy link

@IgorGanapolsky
Copy link

@EugeneTheDev Thank you for this gist. It saved me a lot of time for pulsating dots in Compose.

@mateusvagner
Copy link

Great job!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment