Created
June 11, 2026 10:11
-
-
Save raghunandankavi2010/f9a8d02a5afe97c76740aef7d21aa1e1 to your computer and use it in GitHub Desktop.
Tempearature showing with draggable indicator
This file contains hidden or 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
| private val temperatureAnchors = listOf( | |
| -20f to Color(0xFFB71C1C), // super cold -> deep red | |
| -10f to Color(0xFF1976D2), // cold -> blue | |
| 0f to Color(0xFF26C6DA), // cool -> cyan | |
| 20f to Color(0xFF2E9E5B), // comfortable -> green | |
| 30f to Color(0xFFFB8C00), // warm -> orange | |
| 40f to Color(0xFFD32F2F), // too hot -> red | |
| ) | |
| /** Maps a temperature in °C to its zone color, clamped to the anchor range. */ | |
| fun temperatureColor(temp: Float): Color { | |
| if (temp <= temperatureAnchors.first().first) return temperatureAnchors.first().second | |
| if (temp >= temperatureAnchors.last().first) return temperatureAnchors.last().second | |
| for (i in 0 until temperatureAnchors.size - 1) { | |
| val (t0, c0) = temperatureAnchors[i] | |
| val (t1, c1) = temperatureAnchors[i + 1] | |
| if (temp in t0..t1) { | |
| return lerp(c0, c1, (temp - t0) / (t1 - t0)) | |
| } | |
| } | |
| return temperatureAnchors.last().second | |
| } | |
| @Composable | |
| fun TemperatureChart3( | |
| modifier: Modifier = Modifier, | |
| temp: Int, | |
| minTemp: Int, // Adjusted minTemp considering offset | |
| maxTemp: Int, // Adjusted maxTemp considering offset | |
| textMeasurer: TextMeasurer = rememberTextMeasurer() | |
| ) { | |
| val context = LocalContext.current | |
| val state = remember { mutableFloatStateOf(temp.toFloat()) } // Track indicator position | |
| // ~6 labelled (major) ticks across the range, each split into 5 minor steps. | |
| val majorStep = ((maxTemp - minTemp) / 6f).let { if (it <= 0f) 1f else it } | |
| val minorPerMajor = 5 | |
| val labelStyle = TextStyle( | |
| fontSize = 11.sp, | |
| lineHeight = 13.sp, | |
| fontFamily = AppFontFamilyMedium, | |
| fontWeight = FontWeight(500), | |
| color = Color(0xA6000000), | |
| textAlign = TextAlign.Center, | |
| ) | |
| val badgeStyle = TextStyle( | |
| fontSize = 13.sp, | |
| fontFamily = AppFontFamilyMedium, | |
| fontWeight = FontWeight(700), | |
| color = Color.White, | |
| textAlign = TextAlign.Center, | |
| ) | |
| Canvas( | |
| modifier = modifier | |
| .dragIndicatorModifier2( // Apply the custom modifier | |
| state = state, | |
| minTemp = minTemp.toFloat(), | |
| maxTemp = maxTemp.toFloat(), | |
| onDragEnd = { newTemp -> | |
| val roundedTemp = round(newTemp).toInt() | |
| Toast.makeText(context.applicationContext, "$roundedTemp°", Toast.LENGTH_SHORT).show() | |
| } | |
| ) | |
| ) { | |
| val padX = 12.dp.toPx() | |
| val barRight = size.width - padX | |
| val barWidth = barRight - padX | |
| val barTop = 52.dp.toPx() // leaves room for the pin head above the bar | |
| val barHeight = 38.dp.toPx() | |
| val barBottom = barTop + barHeight | |
| val corner = CornerRadius(10.dp.toPx()) // softly rounded, not a full pill | |
| val range = (maxTemp - minTemp).toFloat().coerceAtLeast(1f) | |
| fun xForTemp(t: Float) = padX + (t - minTemp) / range * barWidth | |
| // 1) Gradient temperature bar — sampled from the zone colors across the range. | |
| val stops = 24 | |
| val gradientColors = (0 until stops).map { i -> | |
| temperatureColor(minTemp + range * i / (stops - 1)) | |
| } | |
| drawRoundRect( | |
| brush = Brush.horizontalGradient(gradientColors, startX = padX, endX = barRight), | |
| topLeft = Offset(padX, barTop), | |
| size = Size(barWidth, barHeight), | |
| cornerRadius = corner, | |
| ) | |
| // 2) Ticks (minor + major) and labels under the bar. | |
| val tickTop = barBottom + 6.dp.toPx() | |
| val minorStep = majorStep / minorPerMajor | |
| val totalMinor = (range / minorStep).toInt() | |
| for (i in 0..totalMinor) { | |
| val tv = minTemp + i * minorStep | |
| if (tv > maxTemp + 0.001f) break | |
| val x = xForTemp(tv) | |
| val isMajor = i % minorPerMajor == 0 | |
| val len = if (isMajor) 12.dp.toPx() else 6.dp.toPx() | |
| drawLine( | |
| color = Color(0x66000000), | |
| start = Offset(x, tickTop), | |
| end = Offset(x, tickTop + len), | |
| strokeWidth = if (isMajor) 2.dp.toPx() else 1.dp.toPx(), | |
| ) | |
| if (isMajor) { | |
| val layout = textMeasurer.measure(tv.toInt().toString(), labelStyle) | |
| drawText( | |
| textLayoutResult = layout, | |
| topLeft = Offset(x - layout.size.width / 2f, tickTop + 14.dp.toPx()), | |
| ) | |
| } | |
| } | |
| // 3) Creative indicator — a teardrop "map pin" that points at the value on | |
| // the bar, tinted to the current zone, with the temperature inside its head. | |
| val value = state.floatValue | |
| val zoneColor = temperatureColor(value) | |
| val cx = xForTemp(value).coerceIn(padX, barRight) | |
| val tipY = barTop + 3.dp.toPx() // tip dips slightly into the bar | |
| val headR = 15.dp.toPx() | |
| val headCy = tipY - headR * 2f // head sits above the bar | |
| val pin = Path().apply { | |
| moveTo(cx, tipY) | |
| // Line up to the lower-left of the head, sweep around the top, back down to the tip. | |
| arcTo( | |
| rect = Rect(center = Offset(cx, headCy), radius = headR), | |
| startAngleDegrees = 135f, | |
| sweepAngleDegrees = 270f, | |
| forceMoveTo = false, | |
| ) | |
| close() | |
| } | |
| // Soft halo so the pin lifts off the background, then fill + white outline. | |
| drawCircle(zoneColor.copy(alpha = 0.18f), radius = headR + 4.dp.toPx(), center = Offset(cx, headCy)) | |
| drawPath(pin, zoneColor) | |
| drawPath(pin, Color.White, style = Stroke(width = 2.5.dp.toPx())) | |
| // Temperature value centered inside the head. | |
| val pinLabel = textMeasurer.measure("${round(value).toInt()}°", badgeStyle) | |
| drawText( | |
| textLayoutResult = pinLabel, | |
| topLeft = Offset( | |
| cx - pinLabel.size.width / 2f, | |
| headCy - pinLabel.size.height / 2f, | |
| ), | |
| ) | |
| // Precise contact dot where the pin meets the bar. | |
| drawCircle(Color.White, radius = 2.5.dp.toPx(), center = Offset(cx, barTop + barHeight / 2f)) | |
| } | |
| } | |
| @Composable | |
| fun Modifier.dragIndicatorModifier( | |
| state: MutableState<Float>, | |
| minTemp: Float, | |
| maxTemp: Float, | |
| indicatorWidth: Dp = 10.dp, | |
| onDragEnd: (Float) -> Unit = {} | |
| ): Modifier { | |
| return this.pointerInput(Unit) { | |
| detectDragGestures( | |
| onDragStart = { | |
| // No action needed on drag start | |
| }, | |
| onDrag = { change, dragAmount -> | |
| val canvasWidth = size.width | |
| val dragRatio = dragAmount.x / canvasWidth.coerceAtLeast(1.toDp().toPx().toInt()) | |
| val positionChange = (maxTemp - minTemp) * dragRatio | |
| val newPosition = state.value + positionChange | |
| val clampedPosition = | |
| max(minTemp, min(maxTemp, newPosition)) | |
| state.value = clampedPosition | |
| }, | |
| onDragEnd = { | |
| onDragEnd(state.value) | |
| } | |
| ) | |
| detectTapGestures( | |
| onTap = { | |
| val canvasSize = size | |
| if (it.x in 0f..(indicatorWidth.toPx() + 5.dp.toPx())) { | |
| val newPosition = (it.x / canvasSize.width) * (maxTemp - minTemp) + minTemp | |
| val clampedPosition = max(minTemp, min(maxTemp, newPosition)) | |
| state.value = clampedPosition | |
| } | |
| } | |
| ) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment