Skip to content

Instantly share code, notes, and snippets.

@fvilarino
Last active June 30, 2022 10:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fvilarino/a370525b02af592554d6b033a5fdfaf5 to your computer and use it in GitHub Desktop.
Save fvilarino/a370525b02af592554d6b033a5fdfaf5 to your computer and use it in GitHub Desktop.
Progress Indicator Final
private const val NumDots = 5
private const val AnimationDuration = 2000
private const val AnimationSegment = AnimationDuration / 10
private val MainDotSize = 24.dp
private val Float.alphaFromRadians: Float
get() {
val normalized = (this / (2f * PI)).toFloat()
return .5f + (normalized - .5f).absoluteValue
}
@Stable
interface ProgressState {
fun start(scope: CoroutineScope)
operator fun get(index: Int): Float
}
class ProgressStateImpl : ProgressState {
private val animationValues: List<MutableState<Float>> = List(NumDots) {
mutableStateOf(0f)
}
override operator fun get(index: Int) = animationValues[index].value
override fun start(scope: CoroutineScope) {
repeat(NumDots) { index ->
scope.launch {
animate(
initialValue = 0f,
targetValue = (2f * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = AnimationDuration
0f at 0
(.5 * PI).toFloat() at 2 * AnimationSegment
PI.toFloat() at 3 * AnimationSegment
(1.5 * PI).toFloat() at 4 * AnimationSegment
(2f * PI).toFloat() at 6 * AnimationSegment
},
repeatMode = RepeatMode.Restart,
initialStartOffset = StartOffset(offsetMillis = 100 * index)
),
) { value, _ ->
animationValues[index].value = value
}
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ProgressStateImpl
if (animationValues != other.animationValues) return false
return true
}
override fun hashCode(): Int = animationValues.hashCode()
}
@Composable
fun rememberProgressState(): ProgressState = remember {
ProgressStateImpl()
}
@Composable
fun ProgressIndicator(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
) {
val state = rememberProgressState()
LaunchedEffect(key1 = Unit) {
state.start(this)
}
Layout(
content = {
val minFactor = .3f
val step = minFactor / NumDots
repeat(NumDots) { index ->
val size = MainDotSize * (1f - step * index)
Dot(
color = color,
modifier = Modifier
.requiredSize(size)
.graphicsLayer {
alpha = state[index].alphaFromRadians
},
)
}
},
modifier = modifier,
) { measurables, constraints ->
val looseConstraints = constraints.copy(
minWidth = 0,
minHeight = 0,
)
val placeables = measurables.map { measurable -> measurable.measure(looseConstraints) }
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
val radius = min(constraints.maxWidth, constraints.maxHeight) / 2f
placeables.forEachIndexed { index, placeable ->
val animatedValue = state[index]
val x = (radius + radius * sin(animatedValue)).roundToInt()
val y = (radius - radius * cos(animatedValue)).roundToInt()
placeable.placeRelative(
x = x,
y = y,
)
}
}
}
}
@Composable
private fun Dot(
color: Color,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.clip(shape = CircleShape)
.background(color = color)
)
}
@Preview(widthDp = 360, showBackground = true)
@Composable
fun PreviewDot() {
ProgressIndicatorTheme {
Dot(
color = MaterialTheme.colors.primary,
modifier = Modifier
.padding(all = 32.dp)
.requiredSize(32.dp)
)
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun PreviewProgressIndicator() {
ProgressIndicatorTheme {
ProgressIndicator(
modifier = Modifier.padding(all = 32.dp)
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment