Skip to content

Instantly share code, notes, and snippets.

@Mikkareem
Last active May 2, 2026 03:40
Show Gist options
  • Select an option

  • Save Mikkareem/fff614fbac2ee0c5c7578637b8896f82 to your computer and use it in GitHub Desktop.

Select an option

Save Mikkareem/fff614fbac2ee0c5c7578637b8896f82 to your computer and use it in GitHub Desktop.
@file:Suppress("FunctionName")
package com.techullurgy.playground
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.center
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.time.Instant
private val HourColor = Color(0xffff3f7f)
private val MinuteColor = Color(0xffffb8de)
private val SecondColor = Color(0xff08cb00)
private val SkinColor = Color(0xff1e104e)
private val SkinPinColor = Color(0xff8c00ff)
private const val HourHandleFixedRotation = 180f
private const val MinuteHandleFixedRotation = 138f
private const val SecondHandleFixedRotation = 240f
private const val HourHandleCloseXFraction = 0.36f
private const val MinuteHandleCloseXFraction = 0.52f
private const val SecondHandleCloseXFraction = 0.57f
private const val HourHandleBump = 60f
private const val MinuteHandleBump = 30f
private const val BarFraction = 0.1f
private const val ClockOutlineWidthDp = 20
private const val ClockPinRadiusDp = 30
private const val HoursShrinkedRectPaddingDp = 20
private const val SecondsShrinkedRectPaddingDp = 75
private const val MinutesShrinkedRectPaddingDp = 120
private const val SecondHandleStrokeWidthDp = 4
private const val BarStrokeWidthDp = 2
private val ClockRotationAnimation = spring<Float>()
@Stable
private data class ClockTime(
val hours: Int,
val minutes: Int,
val seconds: Int
) {
fun hourHandleDegree(): Float = (hours % 12) * 30f + minutes * 0.5f
fun minuteHandleDegree(): Float = minutes * 6f + seconds * 0.1f
fun secondHandleDegree(): Float = seconds * 6f
override fun toString(): String = listOf(
hours.toString().padStart(2, '0'),
minutes.toString().padStart(2, '0'),
seconds.toString().padStart(2, '0'),
).joinToString(":")
}
@Preview
@Composable
fun AnimatedFullScreenClock() {
val textMeasurer = rememberTextMeasurer()
val isInPreview = false //LocalInspectionMode.current
val clockTime by produceState<ClockTime?>(null) {
while (true) {
withInfiniteAnimationFrameMillis {
val currentTime = if(isInPreview) {
Instant.parse("2023-04-27T19:24:47Z").toEpochMilliseconds()
} else System.currentTimeMillis()
val seconds = (currentTime / 1000) % 60
val minutes = (currentTime / (1000 * 60)) % 60
val hours = (currentTime / (1000 * 60 * 60)) % 24
value = ClockTime(
hours = hours.toInt(),
minutes = minutes.toInt(),
seconds = seconds.toInt()
)
}
}
}
val transition = updateTransition(clockTime)
val secondsRotation by transition.animateFloat(
transitionSpec = { ClockRotationAnimation },
targetValueByState = {
it?.secondHandleDegree() ?: 0f
}
)
val minutesRotation by transition.animateFloat(
transitionSpec = { ClockRotationAnimation },
targetValueByState = {
it?.minuteHandleDegree() ?: 0f
}
)
val hoursRotation by transition.animateFloat(
transitionSpec = { ClockRotationAnimation },
targetValueByState = {
it?.hourHandleDegree() ?: 0f
}
)
Canvas(
modifier = Modifier
.fillMaxSize()
.background(Color(0xffff5580))
) {
val clockOvalRect = Rect(
left = size.width * 0.15f,
top = 0f,
right = size.width + (size.width * 0.75f),
bottom = size.height
)
DrawClock(
hoursRotation = hoursRotation,
minutesRotation = minutesRotation,
secondsRotation = secondsRotation,
clockOvalRect = clockOvalRect,
textMeasurer = textMeasurer
)
drawText(
text = "$clockTime",
textMeasurer = textMeasurer,
topLeft = Offset(30.dp.toPx(), 50.dp.toPx()),
style = TextStyle(color = Color.Black, fontWeight = FontWeight.Bold, fontSize = 30.sp)
)
}
}
private fun DrawScope.DrawClock(
hoursRotation: Float,
minutesRotation: Float,
secondsRotation: Float,
clockOvalRect: Rect,
textMeasurer: TextMeasurer
) {
val clockOutline = Path().apply {
addOval(oval = clockOvalRect)
}
drawPath(
path = clockOutline,
color = SkinColor
)
drawPath(
path = clockOutline,
color = SkinPinColor,
style = Stroke(ClockOutlineWidthDp.dp.toPx())
)
DrawClockHourMinSec(
hoursRotation = hoursRotation,
minutesRotation = minutesRotation,
secondsRotation = secondsRotation,
clockOvalRect = clockOvalRect,
textMeasurer = textMeasurer
)
DrawClockHandles(
clockOvalRect = clockOvalRect
)
}
private fun DrawScope.DrawClockHourMinSec(
hoursRotation: Float,
minutesRotation: Float,
secondsRotation: Float,
clockOvalRect: Rect,
textMeasurer: TextMeasurer
) {
val hoursShrinkedRect = clockOvalRect.shrink(HoursShrinkedRectPaddingDp.dp.toPx())
val secondsShrinkedRect = clockOvalRect.shrink(SecondsShrinkedRectPaddingDp.dp.toPx())
val minutesShrinkedRect = clockOvalRect.shrink(MinutesShrinkedRectPaddingDp.dp.toPx())
DrawClockHr(
rotation = hoursRotation + HourHandleFixedRotation,
color = HourColor,
fontSize = 45.sp,
textMeasurer = textMeasurer,
ovalBounds = hoursShrinkedRect
)
DrawClockMinSec(
rotation = secondsRotation + SecondHandleFixedRotation,
color = SecondColor,
fontSize = 25.sp,
textMeasurer = textMeasurer,
ovalBounds = secondsShrinkedRect
)
DrawClockMinSec(
rotation = minutesRotation + MinuteHandleFixedRotation,
color = MinuteColor,
fontSize = 20.sp,
textMeasurer = textMeasurer,
ovalBounds = minutesShrinkedRect
)
}
private fun DrawScope.DrawClockHandles(
clockOvalRect: Rect
) {
// Hour Handle
DrawCurveHandle(
oval = clockOvalRect,
color = HourColor,
rotation = HourHandleFixedRotation,
bump = HourHandleBump,
closeXFraction = HourHandleCloseXFraction,
)
// Second Handle
DrawLineHandle(
oval = clockOvalRect,
color = SecondColor,
rotation = SecondHandleFixedRotation,
strokeWidth = SecondHandleStrokeWidthDp,
closeXFraction = SecondHandleCloseXFraction,
)
// Minute Handle
DrawCurveHandle(
oval = clockOvalRect,
color = MinuteColor,
rotation = MinuteHandleFixedRotation,
bump = MinuteHandleBump,
closeXFraction = MinuteHandleCloseXFraction,
)
drawCircle(
color = SkinPinColor,
radius = ClockPinRadiusDp.dp.toPx(),
center = clockOvalRect.center
)
}
private fun DrawScope.DrawClockMinSec(
rotation: Float,
color: Color,
ovalBounds: Rect,
fontSize: TextUnit,
textMeasurer: TextMeasurer,
barFraction: Float = BarFraction
) {
repeat(60) {
val degree = it.toClockDegree() + rotation
if(it % 5 == 0) {
val minuteText = "$it".padStart(2, '0')
val point = calculatePointOnOvalAtDegree(ovalBounds, degree)
rotate(
degrees = (degree + 90f),
pivot = point
) {
val textLayoutResult = textMeasurer.measure(
text = minuteText,
maxLines = 1,
overflow = TextOverflow.Visible,
style = TextStyle(
fontSize = fontSize,
fontWeight = FontWeight.Bold
)
)
drawText(
textLayoutResult = textLayoutResult,
color = color,
topLeft = point.copy(x = point.x - textLayoutResult.size.center.x)
)
}
} else {
val point = calculatePointOnOvalAtDegree(ovalBounds, degree)
val lineEnd = lerp(point, ovalBounds.center, barFraction)
drawLine(
color = color,
start = point,
end = lineEnd,
strokeWidth = BarStrokeWidthDp.dp.toPx()
)
}
}
}
private fun DrawScope.DrawClockHr(
rotation: Float,
color: Color,
ovalBounds: Rect,
fontSize: TextUnit,
textMeasurer: TextMeasurer
) {
(1..60).forEach {
val degree = it.toClockDegree() + rotation
if(it % 5 == 0) {
val minuteText = "${it / 5}"
val point = calculatePointOnOvalAtDegree(ovalBounds, degree)
rotate(
degrees = (degree + 90f),
pivot = point
) {
val textLayoutResult = textMeasurer.measure(
text = minuteText,
maxLines = 1,
overflow = TextOverflow.Visible,
style = TextStyle(
fontSize = fontSize,
fontWeight = FontWeight.Bold
)
)
drawText(
textLayoutResult = textLayoutResult,
color = color,
topLeft = point.copy(x = point.x - textLayoutResult.size.center.x)
)
}
}
}
}
private fun DrawScope.DrawCurveHandle(
oval: Rect,
color: Color,
rotation: Float,
bump: Float,
closeXFraction: Float
) {
val handPath = provideHandPath(
bump = bump,
startAt = Offset(0f, 0f),
closeAt = Offset(oval.width * closeXFraction, 0f)
)
translate(
left = oval.center.x,
top = oval.center.y
) {
rotate(
rotation,
pivot = Offset.Zero
) {
drawPath(
path = handPath,
color = color,
)
}
}
}
private fun DrawScope.DrawLineHandle(
oval: Rect,
color: Color,
rotation: Float,
strokeWidth: Int,
closeXFraction: Float
) {
translate(
left = oval.center.x,
top = oval.center.y
) {
rotate(
rotation,
pivot = Offset.Zero
) {
drawLine(
start = Offset.Zero,
end = Offset(oval.width * closeXFraction, 0f),
color = color,
strokeWidth = strokeWidth.dp.toPx()
)
}
}
}
private fun Density.provideHandPath(
bump: Float,
startAt: Offset,
closeAt: Offset,
bumpDistance: Float = 70.dp.toPx()
): Path {
return Path().apply {
moveTo(startAt.x, startAt.y)
quadraticTo(startAt.x + bumpDistance, startAt.y, startAt.x + bumpDistance, startAt.y - bump)
lineTo(closeAt.x, closeAt.y)
moveTo(startAt.x, startAt.y)
quadraticTo(startAt.x + bumpDistance, startAt.y, startAt.x + bumpDistance, startAt.y + bump)
lineTo(closeAt.x, closeAt.y)
}
}
private fun calculatePointOnOvalAtDegree(
oval: Rect,
degree: Float
): Offset {
val a = oval.width / 2
val b = oval.height / 2
val cosTheta = cos(degree.toRadians())
val sinTheta = sin(degree.toRadians())
val ab = a * b
val aSquared = a.let { it * it }
val bSquared = b.let { it * it }
val cosSquared = cosTheta.let { it * it }
val sinSquared = sinTheta.let { it * it }
val denominator = sqrt((bSquared * cosSquared) + (aSquared * sinSquared))
val x = (ab * cosTheta) / denominator
val y = (ab * sinTheta) / denominator
return Offset(x.toFloat() + oval.center.x, y.toFloat() + oval.center.y)
}
private fun Rect.shrink(padding: Float): Rect {
return Rect(
left = left + padding,
top = top + padding,
right = right - padding,
bottom = bottom - padding
)
}
private fun Int.toClockDegree() = -(this * 6f)
private fun Float.toRadians() = this * (PI / 180f)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment