Android Create Custom Circular Slider

You are currently viewing Android Create Custom Circular Slider

Below I will show you a sample of how to create a custom circular slider in Android Studio using Jetpack Compose.

The base element used is a Canvas. The Canvas has a method to capture the users touch motions to feed into the value that is being set.

Some of the values set are arbitrary that you may want to change for your own use case in your project.

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
        ) {

        }
    }
}

This is the main Composable function for the CircularSlider.

I added the callback function of

onValueChanged : (Float) -> Unit

So you can send the value captured back to your main project after it has been processed.

@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()
    )
}

Leave a Reply