androidandroid-jetpack-composesliderandroid-jetpack-compose-canvas

How to create a Semicircular Slider using Jetpack Compose canvas?


I am pretty new in Android Studio and need to make a Slider to view data received by an Arduino via Wi-Fi (Temperature and Light). I want to have a SemicircularSlider.kt composable that receives the values of the Arduino and displays them in a range which I will set on the Kotlin file. Then With the Kotlin file I want to add them to my layout in my XML file like it's another View, Or just a Widget and change the values (Like color and range of the slider) through XML.

As I'm new to Composable and Android Studio I tried making the Composable with ChatGPT and gave me This Code

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import kotlin.math.PI
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin

@Composable
fun SemiCircularSlider(
    modifier: Modifier = Modifier,
    value: Float,
    onValueChange: (Float) -> Unit,
    colorStart: Color,
    colorEnd: Color
) {
    var angle by remember { mutableStateOf(0f) }

    Canvas(modifier = modifier
        .fillMaxSize()
        .padding(16.dp)
        .pointerInput(Unit) {
            detectDragGestures { change, _ ->
                val position = change.position
                val center = Offset((size.width / 2).toFloat(), (size.height / 2).toFloat())
                angle = ((atan2(position.y - center.y, position.x - center.x) * 180 / PI).toFloat() + 180) % 180
                onValueChange(angle)
            }
        }
    ) {
        val radius = size.minDimension / 2
        val arcRect = Rect(Offset(size.width / 2 - radius, size.height / 2 - radius), Size(radius * 2, radius * 2))

        drawArc(
            color = colorStart,
            startAngle = 180f,
            sweepAngle = angle,
            useCenter = false,
            style = Stroke(width = 20f),
            topLeft = arcRect.topLeft,
            size = arcRect.size
        )

        val thumbX = center.x + cos((angle - 180) * PI / 180).toFloat() * radius
        val thumbY = center.y + sin((angle - 180) * PI / 180).toFloat() * radius

        drawCircle(
            color = colorEnd,
            radius = 20f,
            center = Offset(thumbX, thumbY)
        )
    }
}

@Composable
fun MainControlScreen() {
    var temperature by remember { mutableStateOf(0f) }
    var light by remember { mutableStateOf(0f) }

    MaterialTheme {
        Column {
            Text("Temperature: ${temperature.toInt()}")
            SemiCircularSlider(
                value = temperature,
                onValueChange = { temperature = it },
                colorStart = Color(0xFFFF5722),
                colorEnd = Color(0xFFFFC107)
            )
            Spacer(modifier = Modifier.height(32.dp))
            Text("Light: ${light.toInt()}")
            SemiCircularSlider(
                value = light,
                onValueChange = { light = it },
                colorStart = Color(0xFF03A9F4),
                colorEnd = Color(0xFF4CAF50)
            )
        }
    }
}

I didn't ask for all about the Arduino connection because I can't even show a semicircular slider on the screen This solution dropped me the error "org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during IR lowering"

This is the idea on How I want my slider to look Temperature Semicircular Slider Idea Only difference would be adding numbers on the leftmost bottom of the slider to show the user a range of values and a center number showing the current value

Feel free to ask any questions if needed or ask for more code. And sorry if my English is a bit off.


Solution

  • Your code (surprisingly ChatGPT gave a good answer) looks fine but it needs some tweaks to make it work.

    First of all setting fillMaxSize() for Canvas is a mistake most of the time. I rather set an arbitrary size for it or set it to a portion of the device width/height but for the sake of just making this work let's just set an arbitrary size for it.

    Canvas(modifier = modifier
            .size(300.dp)
    

    instead of:

    Canvas(modifier = modifier
            .fillMaxSize()
    

    The next problem is that your SemiCircularSlider has no use of the value parameter which is the main data for your Slider/Gauge.

    var angle by remember { mutableFloatStateOf(value) }
    

    instead of:

    var angle by remember { mutableFloatStateOf(0f) }
    

    So fixing the above problems leaves us with this:

    slider