Last active
June 26, 2023 07:11
-
-
Save nikhil-mandlik-dev/6b79071bdde77df792ba9b32941c174b to your computer and use it in GitHub Desktop.
Drawing Steps along the circle
This file contains 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
//data class for wrapping dial customization | |
data class DialStyle( | |
val stepsWidth: Dp = 1.2.dp, | |
val stepsColor: Color = Color.Black, | |
val normalStepsLineHeight: Dp = 8.dp, | |
val fiveStepsLineHeight: Dp = 16.dp, | |
val stepsTextStyle: TextStyle = TextStyle(), | |
val stepsLabelTopPadding: Dp = 12.dp, | |
) | |
data class ClockStyle( | |
val secondsDialStyle: DialStyle = DialStyle(), | |
) | |
@OptIn(ExperimentalTextApi::class) | |
@Composable | |
fun Clock( | |
modifier: Modifier = Modifier.size(320.dp), | |
clockStyle: ClockStyle = ClockStyle() | |
) { | |
val textMeasurer = rememberTextMeasurer() | |
var minuteRotation by remember { mutableStateOf(0f) } | |
var secondRotation by remember { mutableStateOf(0f) } | |
//secondRotation is updated by 6 degree clockwise every one second | |
//here rotation is in negative, in order to get clockwise rotation | |
LaunchedEffect(key1 = true) { | |
while (true) { | |
//in-order to get smooth transition we are updating rotation angle every 16ms | |
//1000ms -> 6 degree | |
//16ms -> 0.096 | |
delay(16) | |
secondRotation -= 0.096f | |
} | |
} | |
//minuteRotation is updated by 0.1 degree clockwise every one second | |
//here rotation is in negative, in order to get clockwise rotation | |
LaunchedEffect(key1 = true) { | |
while (true) { | |
delay(1000) | |
minuteRotation -= 0.1f | |
} | |
} | |
Canvas( | |
modifier = modifier | |
) { | |
val outerRadius = minOf(this.size.width, this.size.height) / 2f | |
val innerRadius = outerRadius - 60.dp.toPx() | |
//Seconds Dial | |
dial( | |
radius = outerRadius, | |
rotation = secondRotation, | |
textMeasurer = textMeasurer, | |
dialStyle = clockStyle.secondsDialStyle | |
) | |
//Minute Dial | |
dial( | |
radius = innerRadius, | |
rotation = minuteRotation, | |
textMeasurer = textMeasurer, | |
dialStyle = clockStyle.minutesDialStyle | |
) | |
} | |
} | |
@OptIn(ExperimentalTextApi::class) | |
fun DrawScope.dial( | |
radius: Float, | |
rotation: Float, | |
textMeasurer: TextMeasurer, | |
dialStyle: DialStyle = DialStyle() | |
) { | |
var stepsAngle = 0 | |
//this will draw 60 steps | |
repeat(60) { steps -> | |
//fiveStep lineHeight > normalStep lineHeight | |
val stepsHeight = if (steps % 5 == 0) { | |
dialStyle.fiveStepsLineHeight.toPx() | |
} else { | |
dialStyle.normalStepsLineHeight.toPx() | |
} | |
//calculate steps, start and end offset | |
val stepsStartOffset = Offset( | |
x = center.x + (radius * cos((stepsAngle + rotation) * (Math.PI / 180f))).toFloat(), | |
y = center.y - (radius * sin((stepsAngle + rotation) * (Math.PI / 180))).toFloat() | |
) | |
val stepsEndOffset = Offset( | |
x = center.x + (radius - stepsHeight) * cos( | |
(stepsAngle + rotation) * (Math.PI / 180) | |
).toFloat(), | |
y = center.y - (radius - stepsHeight) * sin( | |
(stepsAngle + rotation) * (Math.PI / 180) | |
).toFloat() | |
) | |
//draw step | |
drawLine( | |
color = dialStyle.stepsColor, | |
start = stepsStartOffset, | |
end = stepsEndOffset, | |
strokeWidth = dialStyle.stepsWidth.toPx(), | |
cap = StrokeCap.Round | |
) | |
//draw steps labels | |
if (steps % 5 == 0) { | |
//measure the given label width and height | |
val stepsLabel = String.format("%02d", steps) | |
val stepsLabelTextLayout = textMeasurer.measure( | |
text = buildAnnotatedString { append(stepsLabel) }, | |
style = dialStyle.stepsTextStyle | |
) | |
//calculate the offset | |
val stepsLabelOffset = Offset( | |
x = center.x + (radius - stepsHeight - dialStyle.stepsLabelTopPadding.toPx()) * cos( | |
(stepsAngle + rotation) * (Math.PI / 180) | |
).toFloat(), | |
y = center.y - (radius - stepsHeight - dialStyle.stepsLabelTopPadding.toPx()) * sin( | |
(stepsAngle + rotation) * (Math.PI / 180) | |
).toFloat() | |
) | |
//subtract the label width and height to position label at the center of the step | |
val stepsLabelTopLeft = Offset( | |
stepsLabelOffset.x - ((stepsLabelTextLayout.size.width) / 2f), | |
stepsLabelOffset.y - (stepsLabelTextLayout.size.height / 2f) | |
) | |
drawText( | |
textMeasurer = textMeasurer, | |
text = stepsLabel, | |
topLeft = stepsLabelTopLeft, | |
style = dialStyle.stepsTextStyle | |
) | |
} | |
stepsAngle += 6 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment