Last active
September 5, 2024 16:11
-
-
Save decodeandroid/fc6d6907aa66a666bc943591cf56dde4 to your computer and use it in GitHub Desktop.
Analog clock Jetpack Compose
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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