From 7ec1d048c0ae48df1939ef80e122ab071fd689bb Mon Sep 17 00:00:00 2001 From: Shreyash Pattewar <34227385+shreyashp47@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:55:32 +0530 Subject: [PATCH 1/2] Dashboard: clean modern UI redesign (#17) - Single-sensor categories show full-width row cards - Multi-sensor categories show 2-column grid cards - primaryContainer tint cards with no elevation - Clean category headers with uppercase labels and dividers --- .../presentation/dashboard/DashboardScreen.kt | 236 +++++++++++++----- 1 file changed, 178 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/dashboard/DashboardScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/dashboard/DashboardScreen.kt index b1b6b59..44c58cf 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/dashboard/DashboardScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/dashboard/DashboardScreen.kt @@ -21,14 +21,15 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Explore import androidx.compose.material.icons.filled.History -import androidx.compose.material.icons.filled.Navigation import androidx.compose.material.icons.filled.Sensors import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -51,6 +52,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.shreyash.sensorapp.domain.model.SensorAvailability @@ -78,7 +80,12 @@ fun DashboardScreen( Scaffold( topBar = { CenterAlignedTopAppBar( - title = { Text("Sensor") }, + title = { + Text( + "Sensors", + fontWeight = FontWeight.SemiBold + ) + }, actions = { IconButton(onClick = onNavigateToHistory) { Icon(Icons.Default.History, contentDescription = "History") @@ -129,18 +136,14 @@ private fun DashboardScreenContent( LazyVerticalGrid( columns = GridCells.Fixed(2), modifier = modifier.fillMaxSize(), - contentPadding = PaddingValues(12.dp), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { item(span = { GridItemSpan(2) }) { CompassDashboardCard(onClick = onNavigateToCompass) } - item(span = { GridItemSpan(2) }) { - Spacer(Modifier.height(4.dp)) - } - SensorCategory.entries.forEach { category -> val states = grouped[category] ?: return@forEach if (states.isEmpty()) return@forEach @@ -149,15 +152,28 @@ private fun DashboardScreenContent( CategoryHeader(category = category) } - items(states, key = { it.type }) { state -> - SensorGridItem( - state = state, - onClick = { - if (state.availability is SensorAvailability.Available) { - onNavigateToDetail(state.type) + if (states.size == 1) { + item(span = { GridItemSpan(2) }) { + SensorRowItem( + state = states.first(), + onClick = { + if (states.first().availability is SensorAvailability.Available) { + onNavigateToDetail(states.first().type) + } } - } - ) + ) + } + } else { + items(states, key = { it.type }) { state -> + SensorGridItem( + state = state, + onClick = { + if (state.availability is SensorAvailability.Available) { + onNavigateToDetail(state.type) + } + } + ) + } } } } @@ -165,26 +181,122 @@ private fun DashboardScreenContent( @Composable private fun CategoryHeader(category: SensorCategory) { - Row( + Column(modifier = Modifier.fillMaxWidth()) { + Spacer(Modifier.height(8.dp)) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f), + thickness = 0.5.dp + ) + Spacer(Modifier.height(12.dp)) + Text( + text = category.displayName.uppercase(), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + letterSpacing = 0.5.sp + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SensorRowItem( + state: SensorState, + onClick: () -> Unit +) { + val isAvailable = state.availability is SensorAvailability.Available + var showBottomSheet by remember { mutableStateOf(false) } + + Card( modifier = Modifier .fillMaxWidth() - .padding(top = 8.dp), - verticalAlignment = Alignment.CenterVertically + .clickable { + if (isAvailable) onClick() + else showBottomSheet = true + }, + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = if (isAvailable) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.04f) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { - Box( + Row( modifier = Modifier - .width(4.dp) - .height(18.dp) - .clip(RoundedCornerShape(2.dp)) - .background(MaterialTheme.colorScheme.primary) - ) - Spacer(Modifier.width(8.dp)) - Text( - text = category.displayName, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(44.dp) + .clip(CircleShape) + .background( + if (isAvailable) MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f) + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = sensorIcon(state.type), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = if (isAvailable) MaterialTheme.colorScheme.onPrimaryContainer + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.25f) + ) + } + Spacer(Modifier.width(14.dp)) + Column(Modifier.weight(1f)) { + Text( + text = state.type.displayName, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = if (isAvailable) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + ) + if (!isAvailable) { + Spacer(Modifier.height(2.dp)) + Text( + text = "Unavailable", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + ) + } + } + } + } + + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { showBottomSheet = false }, + sheetState = rememberModalBottomSheetState() + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Default.Sensors, + contentDescription = null, + modifier = Modifier.size(56.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + Spacer(Modifier.height(16.dp)) + Text( + text = state.type.displayName, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + Spacer(Modifier.height(12.dp)) + Text( + text = "Your device does not have a ${state.type.displayName}." + + "\n\n${state.type.description}", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(32.dp)) + } + } } } @@ -204,35 +316,35 @@ fun SensorGridItem( if (isAvailable) onClick() else showBottomSheet = true }, - shape = RoundedCornerShape(14.dp), + shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( - containerColor = if (isAvailable) MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f) + containerColor = if (isAvailable) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.04f) ), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(top = 20.dp, bottom = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Box( modifier = Modifier - .size(48.dp) + .size(44.dp) .clip(CircleShape) .background( if (isAvailable) MaterialTheme.colorScheme.primaryContainer - else MaterialTheme.colorScheme.surfaceVariant + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f) ), contentAlignment = Alignment.Center ) { Icon( imageVector = sensorIcon(state.type), contentDescription = null, - modifier = Modifier.size(26.dp), + modifier = Modifier.size(24.dp), tint = if (isAvailable) MaterialTheme.colorScheme.onPrimaryContainer - else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.25f) ) } Spacer(Modifier.height(10.dp)) @@ -243,14 +355,22 @@ fun SensorGridItem( textAlign = TextAlign.Center, maxLines = 1, color = if (isAvailable) MaterialTheme.colorScheme.onSurface - else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) ) if (!isAvailable) { - Text( - text = "Not available", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) - ) + Spacer(Modifier.height(4.dp)) + Box( + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f)) + .padding(horizontal = 8.dp, vertical = 2.dp) + ) { + Text( + text = "Unavailable", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + ) + } } } } @@ -295,11 +415,11 @@ private fun CompassDashboardCard(onClick: () -> Unit) { modifier = Modifier .fillMaxWidth() .clickable(onClick = onClick), - shape = RoundedCornerShape(14.dp), + shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f) ), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { Row( modifier = Modifier @@ -309,24 +429,24 @@ private fun CompassDashboardCard(onClick: () -> Unit) { ) { Box( modifier = Modifier - .size(48.dp) + .size(44.dp) .clip(CircleShape) .background(MaterialTheme.colorScheme.primaryContainer), contentAlignment = Alignment.Center ) { Icon( - imageVector = Icons.Default.Navigation, + imageVector = Icons.Default.Explore, contentDescription = null, - modifier = Modifier.size(26.dp), + modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer ) } - Spacer(Modifier.width(16.dp)) - Column { + Spacer(Modifier.width(14.dp)) + Column(Modifier.weight(1f)) { Text( text = "Compass", style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, + fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface ) Text( @@ -351,7 +471,7 @@ private fun PreviewDashboardScreen() { Scaffold( topBar = { CenterAlignedTopAppBar( - title = { Text("Sensor") }, + title = { Text("Sensors") }, actions = { IconButton(onClick = {}) { Icon(Icons.Default.History, contentDescription = "History") From 4119c4f43155f981d1d3c3c25a4fb23da7972ef0 Mon Sep 17 00:00:00 2001 From: Shreyash Pattewar <34227385+shreyashp47@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:57:55 +0530 Subject: [PATCH 2/2] Gyroscope: improve detail screen UI and fix 3D cube (#4) - Replace animateTo with snapTo in GyroscopeCube (tween was canceling on every sensor update, causing jitter) - Fix depth-sorting alpha to use rotated Z values - Add rotation magnitude gauge with circular arc - Redesign axis values card with colored badges - Add preview composable --- .../presentation/detail/GyroscopeCube.kt | 32 +- .../presentation/detail/GyroscopeScreen.kt | 278 ++++++++++++++++-- 2 files changed, 276 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeCube.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeCube.kt index 95b6bc9..f7303e7 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeCube.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeCube.kt @@ -1,11 +1,9 @@ package com.shreyash.sensorapp.presentation.detail import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card @@ -18,7 +16,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.shreyash.sensorapp.domain.model.SensorReading import kotlin.math.cos @@ -38,16 +35,17 @@ fun GyroscopeCube( val rotationZ = remember { Animatable(0f) } LaunchedEffect(gx, gy, gz) { - rotationX.animateTo(rotationX.value + gx * 0.05f, tween(100)) - rotationY.animateTo(rotationY.value + gy * 0.05f, tween(100)) - rotationZ.animateTo(rotationZ.value + gz * 0.05f, tween(100)) + rotationX.snapTo(rotationX.value + gx * 0.08f) + rotationY.snapTo(rotationY.value + gy * 0.08f) + rotationZ.snapTo(rotationZ.value + gz * 0.08f) } val primary = MaterialTheme.colorScheme.primary + val onSurfaceVariant = MaterialTheme.colorScheme.onSurfaceVariant Card( colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) ), shape = RoundedCornerShape(16.dp) ) { @@ -58,14 +56,14 @@ fun GyroscopeCube( Text( text = "3D Visualization", style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = onSurfaceVariant, modifier = Modifier.padding(top = 8.dp) ) - Canvas(modifier = Modifier.fillMaxSize().padding(bottom = 8.dp)) { + Canvas(modifier = Modifier.fillMaxSize().padding(16.dp)) { val cx = size.width / 2f val cy = size.height / 2f - val s = minOf(cx, cy) * 0.6f + val s = minOf(cx, cy) * 0.55f val rx = rotationX.value val ry = rotationY.value @@ -78,7 +76,7 @@ fun GyroscopeCube( floatArrayOf(1f, 1f, 1f), floatArrayOf(-1f, 1f, 1f) ) - val projected = vertices.map { v -> + val rotated = vertices.map { v -> var x = v[0]; var y = v[1]; var z = v[2] val rxRad = Math.toRadians(rx.toDouble()).toFloat() @@ -101,6 +99,10 @@ fun GyroscopeCube( val yRot = x * sinZ + y * cosZ x = xRot; y = yRot + Triple(x, y, z) + } + + val projected = rotated.map { (x, y, _) -> Offset(cx + x * s, cy + y * s) } @@ -111,18 +113,18 @@ fun GyroscopeCube( ) for ((i, j) in edges) { - val zAvg = (listOf(vertices[i][2], vertices[j][2]).average()).toFloat() - val alpha = ((zAvg + 1f) / 2f).coerceIn(0.3f, 1f) + val zAvg = (rotated[i].third + rotated[j].third) / 2f + val alpha = ((zAvg + 1.5f) / 3f).coerceIn(0.25f, 1f) drawLine( color = primary.copy(alpha = alpha), start = projected[i], end = projected[j], - strokeWidth = 2.dp.toPx() + strokeWidth = 2.5.dp.toPx() ) } for (v in projected) { - drawCircle(color = primary, radius = 3.dp.toPx(), center = v) + drawCircle(color = primary, radius = 3.5.dp.toPx(), center = v) } } } diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeScreen.kt index ca24674..645c14c 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeScreen.kt @@ -1,22 +1,48 @@ package com.shreyash.sensorapp.presentation.detail +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.theme.SensorAppTheme +import com.shreyash.sensorapp.presentation.theme.SensorGreen +import kotlin.math.sqrt @Composable fun GyroscopeScreen( @@ -40,47 +66,261 @@ private fun GyroscopeContent( .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - LiveIndicator() - Spacer(Modifier.height(16.dp)) - LiveValueDisplay( - reading = currentReading, - sensorType = SensorType.GYROSCOPE - ) + if (currentReading != null) { + RotationMagnitudeGauge(reading = currentReading) + + Spacer(Modifier.height(16.dp)) - Spacer(Modifier.height(24.dp)) + AxisValuesCard(reading = currentReading) + } else { + Text( + text = "Waiting for sensor data...", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(Modifier.height(20.dp)) LiveLineChart( readings = chartReadings, sensorType = SensorType.GYROSCOPE, - modifier = Modifier.fillMaxWidth().height(260.dp) + modifier = Modifier.fillMaxWidth().height(240.dp) ) - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(16.dp)) if (currentReading != null) { GyroscopeCube( reading = currentReading, - modifier = Modifier.fillMaxWidth().height(200.dp).padding(16.dp) + modifier = Modifier.fillMaxWidth().height(260.dp) + ) + } + + Spacer(Modifier.height(16.dp)) + + SensorUsageHint(sensorType = SensorType.GYROSCOPE) + + Spacer(Modifier.height(16.dp)) + } +} + +@Composable +private fun RotationMagnitudeGauge(reading: SensorReading) { + val gx = reading.values.getOrNull(0) ?: 0f + val gy = reading.values.getOrNull(1) ?: 0f + val gz = reading.values.getOrNull(2) ?: 0f + val magnitude = sqrt(gx * gx + gy * gy + gz * gz) + + val maxSpeed = 15f + val fraction = (magnitude / maxSpeed).coerceIn(0f, 1f) + + val arcColor = when { + fraction < 0.33f -> SensorGreen + fraction < 0.66f -> Color(0xFFFFC107) + else -> Color(0xFFE53935) + } + val arcBgColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f) + + Card( + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "ROTATION SPEED", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + letterSpacing = 0.5.sp ) + Spacer(Modifier.height(16.dp)) + + Box( + modifier = Modifier.size(160.dp), + contentAlignment = Alignment.Center + ) { + Canvas(modifier = Modifier.fillMaxSize()) { + val strokeWidth = 14.dp.toPx() + val arcSize = Size(size.width - strokeWidth, size.height - strokeWidth) + val topLeft = Offset(strokeWidth / 2f, strokeWidth / 2f) + + drawArc( + color = arcBgColor, + startAngle = 135f, + sweepAngle = 270f, + useCenter = false, + topLeft = topLeft, + size = arcSize, + style = Stroke(strokeWidth, cap = StrokeCap.Round) + ) + + drawArc( + color = arcColor, + startAngle = 135f, + sweepAngle = 270f * fraction, + useCenter = false, + topLeft = topLeft, + size = arcSize, + style = Stroke(strokeWidth, cap = StrokeCap.Round) + ) + } + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = String.format("%.1f", magnitude), + style = MaterialTheme.typography.displayMedium, + fontWeight = FontWeight.Bold, + color = arcColor + ) + Text( + text = "°/s", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + } + Spacer(Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + SpeedLabel("Slow", SensorGreen) + SpeedLabel("Moderate", Color(0xFFFFC107)) + SpeedLabel("Fast", Color(0xFFE53935)) + } } + } +} +@Composable +private fun SpeedLabel(text: String, color: Color) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(6.dp) + .clip(RoundedCornerShape(3.dp)) + .background(color) + ) + Spacer(Modifier.width(4.dp)) Text( - text = "X: ${formatDetailValue(currentReading?.values?.getOrNull(0))} ${SensorType.GYROSCOPE.unitX}" + - " Y: ${formatDetailValue(currentReading?.values?.getOrNull(1))} ${SensorType.GYROSCOPE.unitY}" + - " Z: ${formatDetailValue(currentReading?.values?.getOrNull(2))} ${SensorType.GYROSCOPE.unitZ}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + text = text, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) + } +} - Spacer(Modifier.height(16.dp)) +@Composable +private fun AxisValuesCard(reading: SensorReading) { + Card( + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "AXIS VALUES", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + letterSpacing = 0.5.sp + ) - SensorUsageHint(sensorType = SensorType.GYROSCOPE) + Spacer(Modifier.height(16.dp)) - Spacer(Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + AxisValueCard( + label = "X", + value = reading.values.getOrNull(0) ?: 0f, + unit = SensorType.GYROSCOPE.unitX, + color = Color(0xFF42A5F5) + ) + AxisValueCard( + label = "Y", + value = reading.values.getOrNull(1) ?: 0f, + unit = SensorType.GYROSCOPE.unitY, + color = SensorGreen + ) + AxisValueCard( + label = "Z", + value = reading.values.getOrNull(2) ?: 0f, + unit = SensorType.GYROSCOPE.unitZ, + color = Color(0xFFFF7043) + ) + } + } + } +} + +@Composable +private fun AxisValueCard( + label: String, + value: Float, + unit: String, + color: Color +) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(12.dp)) + .background(color.copy(alpha = 0.15f)), + contentAlignment = Alignment.Center + ) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = color + ) + } + Spacer(Modifier.height(8.dp)) + Text( + text = String.format("%.1f", value), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = unit, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF1A1C1E) +@Composable +private fun PreviewGyroscopeScreen() { + SensorAppTheme { + GyroscopeContent( + currentReading = SensorReading( + sensorType = SensorType.GYROSCOPE, + values = listOf(1.2f, -0.8f, 0.5f), + accuracy = 3, + timestampMs = System.currentTimeMillis() + ), + chartReadings = emptyList() + ) } }