SUBSCRIBE NOW
I always learn something just by skimming it that makes me want to bookmark the issue now and dig deeper later
SUBSCRIBE NOW
Keep up the good work with the newsletter 💪 I really enjoy it
SUBSCRIBE NOW
Dispatch is a must read for Android devs today and my go-to for keeping up with all things Jetpack Compose
SUBSCRIBE NOW
Dispatch has been my go-to resource as it's packed with useful information while being fun at the same time
SUBSCRIBE NOW
The content is light, fun, and still useful. I especially appreciate the small tips that are in each issue
SUBSCRIBE NOW
I truly love this newsletter ❤️🔥 Spot on content and I know there's a lot of effort that goes behind it
SUBSCRIBE NOW
Thanks for taking the time and energy to do it so well
JetpackCompose.app's Newsletter
I always learn something just by skimming it that makes me want to bookmark the issue now and dig deeper later
JetpackCompose.app's Newsletter
Keep up the good work with the newsletter 💪 I really enjoy it
JetpackCompose.app's Newsletter
Dispatch is a must read for Android devs today and my go-to for keeping up with all things Jetpack Compose
JetpackCompose.app's Newsletter
Dispatch has been my go-to resource as it's packed with useful information while being fun at the same time
JetpackCompose.app's Newsletter
The content is light, fun, and still useful. I especially appreciate the small tips that are in each issue
JetpackCompose.app's Newsletter
I truly love this newsletter ❤️🔥 Spot on content and I know there's a lot of effort that goes behind it
JetpackCompose.app's Newsletter
Thanks for taking the time and energy to do it so well
Animated Loader Component
Author: Eugene Seager
Common dots loading animations implemented using Jetpack Compose. Here's a demo of what these look like in action
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() | |
} | |
} |
Have a project you'd like to submit? Fill this form, will ya!
If you like this snippet, you might also like:
Maker OS is an all-in-one productivity system for developers
I built Maker OS to track, manage & organize my life. Now you can do it too!