Skip to content

Instantly share code, notes, and snippets.

@Kyriakos-Georgiopoulos
Last active October 28, 2025 11:09
Show Gist options
  • Save Kyriakos-Georgiopoulos/8cc955694bb9fea6402bdb20f714f995 to your computer and use it in GitHub Desktop.
Save Kyriakos-Georgiopoulos/8cc955694bb9fea6402bdb20f714f995 to your computer and use it in GitHub Desktop.
/*
* Copyright 2025 Kyriakos Georgiopoulos
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.view.animation.BounceInterpolator
import androidx.annotation.DrawableRes
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.zengrip.R
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.sqrt
@Composable
fun PreviewDiceRoller() {
MaterialTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(Color(0xFFECECEC), Color(0xFFDDDDDD)),
startY = 0f,
endY = Float.POSITIVE_INFINITY
)
),
contentAlignment = Alignment.Center
) {
DiceRoller()
}
}
}
val BounceInterpolatorEasing = Easing {
BounceInterpolator().getInterpolation(it)
}
@Composable
fun DiceRoller(
modifier: Modifier = Modifier,
size: Dp = 160.dp
) {
var currentFace by remember { mutableIntStateOf(1) }
var finalFace by remember { mutableIntStateOf(1) }
var isRolling by remember { mutableStateOf(false) }
val rotation = remember { Animatable(0f) }
val scale = remember { Animatable(1f) }
val scope = rememberCoroutineScope()
fun rollDice() {
if (isRolling) return
isRolling = true
finalFace = (1..6).random()
scope.launch {
val duration = 1000
// Start rotation and scale bounce
val rot = launch {
rotation.animateTo(
720f,
animationSpec = tween(duration, easing = LinearEasing)
)
}
launch {
scale.animateTo(0.85f, tween(150, easing = FastOutLinearInEasing))
scale.animateTo(1.05f, tween(250, easing = FastOutSlowInEasing))
scale.animateTo(1f, tween(300, easing = BounceInterpolatorEasing))
}
val dur = duration / 2
delay(dur.toLong())
currentFace = finalFace
rot.join()
rotation.snapTo(0f)
isRolling = false
}
}
OnShake(onShake = ::rollDice)
Box(
modifier = modifier.size(size),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.matchParentSize()
.padding(top = 12.dp)
.blur(10.dp)
.background(Color(0x22000000), shape = CircleShape)
)
Box(
modifier = Modifier
.matchParentSize()
.graphicsLayer {
rotationZ = rotation.value
scaleX = scale.value
scaleY = scale.value
cameraDistance = 16 * density
}
.shadow(10.dp, RoundedCornerShape(16.dp))
.background(Color(0xFFFDFDFD), RoundedCornerShape(16.dp))
.clickable(enabled = !isRolling) { rollDice() },
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = getDiceImage(currentFace)),
contentDescription = "Dice face $currentFace",
modifier = Modifier.fillMaxSize(0.85f)
)
}
}
}
@Composable
fun OnShake(
shakeThreshold: Float = 12f,
onShake: () -> Unit
) {
val context = LocalContext.current
val sensorManager =
remember { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager }
val sensor = remember { sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) }
val lastShakeTime = remember { mutableLongStateOf(0L) }
DisposableEffect(sensorManager) {
val listener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
val x = event.values[0]
val y = event.values[1]
val z = event.values[2]
val acceleration = sqrt(x * x + y * y + z * z) - SensorManager.GRAVITY_EARTH
if (acceleration > shakeThreshold) {
val now = System.currentTimeMillis()
if (now - lastShakeTime.longValue > 1000) { // throttle
lastShakeTime.longValue = now
onShake()
}
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit
}
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI)
onDispose {
sensorManager.unregisterListener(listener)
}
}
}
@DrawableRes
fun getDiceImage(number: Int): Int = when (number) {
1 -> R.drawable.dice_1
2 -> R.drawable.dice_2
3 -> R.drawable.dice_3
4 -> R.drawable.dice_4
5 -> R.drawable.dice_5
6 -> R.drawable.dice_6
else -> R.drawable.dice_1
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment