Skip to content

Instantly share code, notes, and snippets.

@KlassenKonstantin
Last active March 21, 2023 09:35
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save KlassenKonstantin/788962b8dbe357ec0bb7ca311626f5b3 to your computer and use it in GitHub Desktop.
Save KlassenKonstantin/788962b8dbe357ec0bb7ca311626f5b3 to your computer and use it in GitHub Desktop.
// Usage
// PingPongSwitch(
// textFirst = "Ping",
// textSecond = "Pong"
// )
@Composable
fun PingPongSwitch(
state: PingPongSwitchState = rememberPingPongSwitchState(),
textFirst: String,
textSecond: String,
) {
Box(
modifier = Modifier
.pingPongSwitchPointer(state)
.graphicsLayer {
cameraDistance = 16f
rotationY = state.tiltProgress.value * 16
},
) {
Box(
modifier = Modifier
.width(IntrinsicSize.Max)
) {
Row(
Modifier
.width(300.dp)
.border(1.dp, color, RoundedCornerShape(50))
.clip(RoundedCornerShape(50))
) {
Text(
modifier = Modifier
.padding(vertical = 12.dp)
.weight(1f), text = textFirst, color = color, textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge
)
Text(
modifier = Modifier
.padding(vertical = 12.dp)
.weight(1f),
text = textSecond, color = color, textAlign = TextAlign.Center
)
}
Box(modifier = Modifier
.graphicsLayer {
cameraDistance = 16f
transformOrigin = TransformOrigin(1f, 0.5f)
rotationY = state.flipRotation.value
}
.background(color, RoundedCornerShape(CornerSize(50), CornerSize(0), CornerSize(0), CornerSize(50)))
.fillMaxWidth(0.5f),
contentAlignment = Alignment.Center
) {
Text(
modifier = Modifier
.padding(vertical = 12.dp)
.graphicsLayer {
rotationY = if (state.flipRotation.value <= 90f) 0f else 180f
},
text = if (state.flipRotation.value <= 90f) textFirst else textSecond,
color = Color.White,
textAlign = TextAlign.Center
)
}
}
}
}
fun Modifier.pingPongSwitchPointer(state: PingPongSwitchState) = pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
do {
val event = awaitPointerEvent()
val xRelative = event.changes.first().position.x / size.width
state.setTouchTarget(if (xRelative <= 0.5f) PingPongSwitchState.TouchTarget.FIRST else PingPongSwitchState.TouchTarget.SECOND)
} while (event.type != PointerEventType.Release)
state.setTouchTarget(PingPongSwitchState.TouchTarget.NONE)
}
}
}
@Composable
fun rememberPingPongSwitchState(
initialSelection: PingPongSwitchState.Selection = PingPongSwitchState.Selection.FIRST,
): PingPongSwitchState {
val scope = rememberCoroutineScope()
return remember(initialSelection) { PingPongSwitchState(initialSelection, scope) }
}
class PingPongSwitchState(
initialSelection: Selection,
private val scope: CoroutineScope
) {
val selection = mutableStateOf(initialSelection)
val tiltProgress = Animatable(0f)
val flipRotation = Animatable(0f)
init {
scope.launch {
setSelection(initialSelection, false)
}
}
fun setSelection(selection: Selection, animate: Boolean) {
this.selection.value = selection
val targetRotation = if (selection == Selection.FIRST) 0f else 180f
scope.launch {
if (animate) {
flipRotation.animateTo(
targetRotation,
spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessLow)
)
} else {
flipRotation.snapTo(targetRotation)
}
}
}
fun setTouchTarget(touchTarget: TouchTarget) {
scope.launch {
tiltProgress.animateTo(
when (touchTarget) {
TouchTarget.FIRST -> -1f
TouchTarget.SECOND -> 1f
TouchTarget.NONE -> 0f
},
spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow)
)
}
when (touchTarget) {
TouchTarget.FIRST -> setSelection(Selection.FIRST, true)
TouchTarget.SECOND -> setSelection(Selection.SECOND, true)
TouchTarget.NONE -> {}
}
}
enum class Selection {
FIRST, SECOND
}
enum class TouchTarget {
FIRST, SECOND, NONE
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment