-
-
Save jefisu/9306b9dbe0d18e304e7b32120846115f to your computer and use it in GitHub Desktop.
Dots loading animations with Jetpack Compose
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment