Android Circular Slider
Process
The base element used is a Canvas. The Canvas has a method to capture the users touch coordinates to feed into a value for further processing.
Some of the values set are arbitrary that you may want to change for your own use case in your project.
Code
MainActivity
This is the MainActivity.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SampleCircularSliderTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
CircularSlider(
initialAngle = 54f, // 15%
desiredSize = 300,
handleColor = Color.Blue,
inactiveTrackColor = Color.Red.copy(alpha = 0.1f),
activeTrackColor = Color.Green,
fontSize = 35,
fontColor = Color.Black,
modifier = Modifier.padding(innerPadding)
) {
}
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
SampleCircularSliderTheme {
CircularSlider(
initialAngle = 54f,
desiredSize = 300,
handleColor = Color.Blue,
inactiveTrackColor = Color.Red.copy(alpha = 0.1f),
activeTrackColor = Color.Green,
fontSize = 35,
fontColor = Color.Black
) {
}
}
}CircularSlider
This is the main composable function for the Android Circular Slider.
Breakdown
There are a few functions added to help with calculations.
This function takes the center coordinates of the custom element as well as the user dragoffset coordinates to calculate the angle of the circle the user is interacting with.
This function takes the user dragoffset coordinates the calculate where to place the handle at the perimeter of the circle.
Callback
I added a callback function so you can send the value captured back to your main project after it has been processed.
onValueChanged : (Float) -> Unit@Composable
fun CircularSlider(
initialAngle: Float,
desiredSize: Int,
handleColor : Color,
inactiveTrackColor: Color,
activeTrackColor: Color,
fontSize: Int,
fontColor: Color,
modifier: Modifier = Modifier,
onValueChanged : (Float) -> Unit
) {
var dragOffset by remember {
mutableStateOf(Offset.Zero)
}
var newAngle by remember {
mutableStateOf(initialAngle)
}
var mSize by remember {
mutableStateOf(IntSize.Zero)
}
val textMeasurer = rememberTextMeasurer()
val animateBGColor by animateColorAsState(targetValue = if(newAngle <= 52){ Color.Red.copy(alpha = 0.1f)} else { activeTrackColor.copy(alpha = 0.1f)})
Canvas(
modifier = modifier
.size(desiredSize.dp)
.padding(30.dp)
.onGloballyPositioned {
mSize = it.size
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
dragOffset = change.position
newAngle = calculateNewAngle(mSize.center.toOffset(), dragOffset)
}
}
.pointerInput(Unit) {
detectTapGestures(
onPress = {
dragOffset = it
newAngle = calculateNewAngle(mSize.center.toOffset(), dragOffset)
}
)
}
) {
drawCircle(
brush = Brush.radialGradient(
0.0f to Color.Transparent,
0.25f to animateBGColor,
1.0f to Color.Black.copy(alpha = 0.25f),
),
radius = mSize.width.toFloat() / 2 - 10
)
drawCircle(
color = inactiveTrackColor,
radius = size.width / 2,
center = center,
style = Stroke(
20f,
cap = StrokeCap.Round
)
)
drawArc(
color = animateBGColor.copy(alpha = 1.0f),
startAngle = 0f,
sweepAngle = newAngle,
style = Stroke(
30f,
cap = StrokeCap.Round
),
useCenter = false
)
drawCircle(
color = handleColor,
radius = 30f,
center = calculateHandleOffset(mSize, newAngle),
style = Fill
)
drawContext.canvas.nativeCanvas.apply {
drawText(
textMeasurer = textMeasurer,
text = "${(newAngle / 360 * 100).roundToInt()}%",
topLeft = Offset(center.x - fontSize * 2, center.y - fontSize * 2),
style = TextStyle(
fontSize = fontSize.sp,
textAlign = TextAlign.Center,
color = fontColor
)
)
}
}
}
fun calculateNewAngle(mCenter: Offset, dragOffset: Offset): Float {
val rad = atan2((mCenter.y - dragOffset.y).toDouble(), (mCenter.x - dragOffset.x).toDouble())
val angle = Math.toDegrees(rad)
return (angle + 180f).roundToInt().toFloat()
}
fun calculateHandleOffset(size: IntSize, newAngle: Float): Offset {
return Offset(
(size.center.x + Math.cos(Math.toRadians(newAngle.toDouble())) * (size.width / 2)).toFloat(),
(size.center.y + Math.sin(Math.toRadians(newAngle.toDouble())) * (size.height / 2)).toFloat()
)
}Hope this was useful.

