Skip to content

Instantly share code, notes, and snippets.

@raghunandankavi2010
Created June 11, 2026 10:11
Show Gist options
  • Select an option

  • Save raghunandankavi2010/f9a8d02a5afe97c76740aef7d21aa1e1 to your computer and use it in GitHub Desktop.

Select an option

Save raghunandankavi2010/f9a8d02a5afe97c76740aef7d21aa1e1 to your computer and use it in GitHub Desktop.
Tempearature showing with draggable indicator
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