Skip to content

Instantly share code, notes, and snippets.

@decodeandroid
Last active September 5, 2024 16:11
Show Gist options
  • Select an option

  • Save decodeandroid/fc6d6907aa66a666bc943591cf56dde4 to your computer and use it in GitHub Desktop.

Select an option

Save decodeandroid/fc6d6907aa66a666bc943591cf56dde4 to your computer and use it in GitHub Desktop.
Analog clock Jetpack Compose
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.learngit.ui.theme.gray
import com.example.learngit.ui.theme.redOrange
import com.example.learngit.ui.theme.white
import kotlinx.coroutines.delay
import java.util.Calendar
import java.util.Date
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun PreviewClock(modifier: Modifier = Modifier) {
AnalogClock()
}
@Composable
fun AnalogClock() {
var currentTimeInMs by remember {
mutableLongStateOf(System.currentTimeMillis())
}
val textMeasurer = rememberTextMeasurer()
var circleCenter by remember {
mutableStateOf(Offset.Zero)
}
val circleRadius = 450f
//it will run once
LaunchedEffect(key1 = true) {
//it will run in loop continue after every one second
while (true) {
delay(1000)
currentTimeInMs = System.currentTimeMillis()
}
}
Box(
modifier = Modifier
.background(white)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Canvas(
modifier = Modifier
.fillMaxSize()
) {
//height and width of canvas
val width = size.width
val height = size.height
//exact at the centre of canvas
circleCenter = Offset(x = width / 2f, y = height / 2f)
val date = Date(currentTimeInMs)
val calendar = Calendar.getInstance()
//set the current time in the calendar
calendar.time = date
//now its easy to find hours, min & seconds from calendar
val hours = calendar.get(Calendar.HOUR_OF_DAY)
val minutes = calendar.get(Calendar.MINUTE)
val seconds = calendar.get(Calendar.SECOND)
val brush = Brush.radialGradient(
listOf(
Color.White.copy(0.45f),
Color.Cyan.copy(0.35f),
)
)
//the outer frame circle
drawCircle(
style = Stroke(
width = 15f
),
brush = brush,
radius = circleRadius + 7f,
center = circleCenter
)
//it is the inner circle
drawCircle(
brush = brush,
radius = circleRadius,
center = circleCenter
)
//draw the seconds & minutes lines around the clock
val littleLineLength = circleRadius * 0.08f
val largeLineLength = circleRadius * 0.11f
val textCircleRadius = circleRadius - 80f
//we want 60 line to show
for (i in 0 until 60) {
//angle for each line
val angleInDegrees = i * 360f / 60 //6 degree gap b/w each
val angleInRad = Math.toRadians(angleInDegrees.toDouble()) + PI / 2f
//we want main points with long line and other with short lines
//so multiple of 5 will be long line
val lineLength = if (i % 5 == 0) largeLineLength else littleLineLength
val lineThickness = if (i % 5 == 0) 5f else 2f
val lineColor = if (i % 5 == 0) Color.Cyan else Color.Gray
val start = Offset(
x = (circleRadius * cos(angleInRad) + circleCenter.x).toFloat(),
y = (circleRadius * sin(angleInRad) + circleCenter.y).toFloat()
)
val end = Offset(
x = (circleRadius * cos(angleInRad) + circleCenter.x).toFloat(),
y = (circleRadius * sin(angleInRad) + lineLength + circleCenter.y).toFloat()
)
rotate(
angleInDegrees + 180,
pivot = start
) {
drawLine(
color = lineColor,
start = start,
end = end,
strokeWidth = lineThickness.dp.toPx(),
cap = StrokeCap.Butt
)
}
val rotateAngle = if (angleInDegrees == 0f) 360f else angleInDegrees
drawContext.canvas.nativeCanvas.apply {
if (i % 5 == 0) {
val midAngle = rotateAngle - 90f
val midOffSet = Offset(
x = (cos(Math.toRadians(midAngle.toDouble())) * textCircleRadius + circleCenter.x).toFloat(),
y = (sin(Math.toRadians(midAngle.toDouble())) * textCircleRadius + circleCenter.y).toFloat()
)
//measure text if it have any style like font, size, letter spacing
val textLayoutResult = textMeasurer.measure(
text = (rotateAngle / 30).toInt().toString(),
style = TextStyle.Default.copy(fontSize = 15.sp)
)
val textWidth = textLayoutResult.size.width
val textHeight = textLayoutResult.size.height
drawText(
textLayoutResult, color = Color.Black,
topLeft = Offset(
midOffSet.x - textWidth / 2,
midOffSet.y - textHeight / 2
)
)
}
}
}
val clockHands = listOf(ClockHands.Seconds, ClockHands.Minutes, ClockHands.Hours)
clockHands.forEach { clockHand ->
val eachDegree = 360f / 60f // 6
// to get angle of each hand at a moment
val angleInDegrees = when (clockHand) {
ClockHands.Seconds -> {
seconds * eachDegree
}
ClockHands.Minutes -> {
(minutes + seconds / 60f) * eachDegree
}
ClockHands.Hours -> {
//If the time is 3:30:
//Hours contribution: 60 * 3 = 180 minutes
//Total minutes = 180 + 30 = 210 minutes
//Angle = 0.5 * 210 = 105 degrees*/
(60 * hours + minutes) * 30f / 60f
}
}
val lineLength = when (clockHand) {
ClockHands.Seconds -> {
circleRadius * 0.8f
}
ClockHands.Minutes -> {
circleRadius * 0.7f
}
ClockHands.Hours -> {
circleRadius * 0.5f
}
}
val lineThickness = when (clockHand) {
ClockHands.Seconds -> {
3f
}
ClockHands.Minutes -> {
7f
}
ClockHands.Hours -> {
9f
}
}
val start = Offset(
x = circleCenter.x,
y = circleCenter.y
)
val end = Offset(
x = circleCenter.x,
y = lineLength + circleCenter.y
)
rotate(
angleInDegrees - 180,
pivot = start
) {
drawLine(
color = if (clockHand == ClockHands.Seconds) redOrange else gray,
start = start,
end = end,
strokeWidth = lineThickness.dp.toPx(),
cap = StrokeCap.Round
)
}
}
//center circle
drawCircle(
color = Color.Cyan,
radius = 35f,
center = circleCenter
)
}
}
}
enum class ClockHands {
Seconds,
Minutes,
Hours
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment