Skip to content

Instantly share code, notes, and snippets.

@remziakgoz
Created July 6, 2025 15:11
Show Gist options
  • Select an option

  • Save remziakgoz/fd6ed82180aa57694aa75feb5d5c244d to your computer and use it in GitHub Desktop.

Select an option

Save remziakgoz/fd6ed82180aa57694aa75feb5d5c244d to your computer and use it in GitHub Desktop.
Jetpack Compose animation demo of a molecular-style radial menu with rotating buttons, directional detail popups, and interactive motion using Animatable, LaunchedEffect, and Offset geometry.
@Composable
fun MolekulerMenu() {
var center by remember { mutableStateOf(Offset.Zero) }
var expanded by remember { mutableStateOf(false) }
var selectedIndex by remember { mutableStateOf<Int?>(null) }
val radius = remember { Animatable(0f) }
val rotation = remember { Animatable(0f) }
LaunchedEffect(center) {
if (expanded) {
radius.snapTo(0f)
radius.animateTo(150f, tween(500))
}
}
LaunchedEffect(expanded) {
if (expanded) {
while (true) {
rotation.animateTo(
rotation.value + 360f,
animationSpec = tween(durationMillis = 8000, easing = LinearEasing)
)
}
}
}
BackHandler(enabled = expanded || selectedIndex != null) {
if (selectedIndex != null) {
selectedIndex = null
} else {
expanded = false
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF101010))
.pointerInput(Unit) {
detectTapGestures { offset ->
if (!expanded) {
center = offset
expanded = true
}
}
}
) {
if (!expanded) {
Text(
"TAP",
color = Color.White,
fontSize = 24.sp,
modifier = Modifier
.align(Alignment.Center)
.graphicsLayer {
alpha = ((sin(System.currentTimeMillis() / 300.0) + 1.0) / 2.0).toFloat()
}
)
}
AnimatedVisibility(
visible = selectedIndex != null,
enter = fadeIn(tween(300)) + scaleIn(),
exit = fadeOut(tween(300)) + scaleOut()
) {
selectedIndex?.let {
Box(
modifier = Modifier
.align(Alignment.Center)
.size(200.dp)
.background(Color(0xFF222222), shape = CircleShape),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Detail: ${it + 1}",
color = Color.Yellow,
fontSize = 24.sp
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { selectedIndex = null }) {
Text("Back")
}
}
}
}
}
if (expanded) {
Box(
modifier = Modifier
.size(60.dp)
.offset { IntOffset(center.x.toInt() - 45, center.y.toInt() - 45) }
.background(Color.Cyan, CircleShape)
)
val count = 6
repeat(count) { index ->
val baseAngle = (2 * PI / count) * index
val dynamicAngle = baseAngle + Math.toRadians(rotation.value.toDouble())
val x = center.x + radius.value * cos(dynamicAngle)
val y = center.y + radius.value * sin(dynamicAngle)
val isVisible = selectedIndex == null || selectedIndex == index
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(tween(300)) + scaleIn(),
exit = fadeOut(tween(300)) + scaleOut()
) {
val isSelected = selectedIndex == index
Box(
modifier = Modifier
.size(if (isSelected) 60.dp else 40.dp)
.offset { IntOffset(x.toInt() - 20, y.toInt() - 20) }
.background(
color = if (isSelected) Color.Yellow else Color.Magenta,
shape = CircleShape
)
.clickable {
selectedIndex = index
},
contentAlignment = Alignment.Center
) {
Text(
text = "${index + 1}",
color = Color.White,
fontSize = if (isSelected) 20.sp else 16.sp
)
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment