Skip to content

Instantly share code, notes, and snippets.

@tomriddle25
Forked from amayde/MechanicalRedSwitch.kt
Created October 28, 2025 08:13
Show Gist options
  • Select an option

  • Save tomriddle25/ba4da99e3ed546218224e0c0ecd3e517 to your computer and use it in GitHub Desktop.

Select an option

Save tomriddle25/ba4da99e3ed546218224e0c0ecd3e517 to your computer and use it in GitHub Desktop.
Mechanical red switch - jetpack compose
package com.example.switch
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.draw.dropShadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RadialGradientShader
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shader
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.shadow.Shadow
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
@Composable
fun MechanicalSwitchBackground(
modifier: Modifier,
isOn: Boolean,
button: @Composable BoxScope.() -> Unit
) {
val isOnFloat by animateFloatAsState(
targetValue = if (isOn) 0f else 1f,
animationSpec = spring(
stiffness = Spring.StiffnessHigh
)
)
val largeRadialGradient = object : ShaderBrush() {
override fun createShader(size: Size): Shader {
val biggerDimension = maxOf(size.height, size.width)
return RadialGradientShader(
colors = listOf(
lerp(Color(0xFFFF960A), Color(0x99681401), isOnFloat),
lerp(Color(0xFFFF6E04), Color(0x99681401), isOnFloat),
lerp(Color(0xFFF02305), Color(0x99681401), isOnFloat),
lerp(Color(0xFF962801), Color(0x99681401), isOnFloat),
lerp(Color(0x99681401), Color(0x99681401), isOnFloat),
),
center = size.center,
radius = biggerDimension / 2f,
colorStops = listOf(0f, 0.2f, 0.4f, 0.75f, 1f)
)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF1E2022)),
contentAlignment = Alignment.Center
) {
Box(
modifier = modifier
.height(400.dp)
.width(200.dp)
.dropShadow(
shape = RoundedCornerShape(16.dp),
shadow = Shadow(
radius = 30.dp,
spread = 5.dp,
color = Color(0x66000000),
offset = DpOffset(x = 0.dp, 30.dp)
)
)
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.padding(8.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.dropShadow(
shape = RoundedCornerShape(12.dp),
shadow = Shadow(
radius = 12.dp,
spread = 2.dp,
brush = Brush.verticalGradient(
colorStops = arrayOf(
0.0f to Color(0x66000000),
0.03f to Color(0x66000000),
0.45f to lerp(Color(0xBFF02305), Color(0x66000000), isOnFloat),
0.55f to lerp(Color(0xBFF02305), Color(0x66000000), isOnFloat),
0.97f to Color(0x66000000),
1f to Color(0x66000000),
)
),
offset = DpOffset(x = 0.dp, 0.dp)
)
)
.clip(RoundedCornerShape(12.dp))
.background(Color.LightGray.copy(alpha = 0.8f))
.padding(10.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.dropShadow(
shape = RoundedCornerShape(12.dp),
shadow = Shadow(
radius = 24.dp,
spread = 6.dp,
brush = Brush.verticalGradient(
colorStops = arrayOf(
0.0f to Color(0x66000000),
0.03f to Color(0x66000000),
0.45f to lerp(Color(0xBFF02305), Color(0x66000000), isOnFloat),
0.55f to lerp(Color(0xBFF02305), Color(0x66000000), isOnFloat),
0.97f to Color(0x66000000),
1f to Color(0x66000000),
)
),
offset = DpOffset(x = 0.dp, 0.dp)
)
)
.clip(RoundedCornerShape(12.dp))
.background(Color.Black)
.padding(vertical = 10.dp, horizontal = 8.dp)
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(6.dp))
.fillMaxSize()
.background(largeRadialGradient)
) {
button.invoke(this)
}
}
}
}
}
}
@Composable
fun MechanicalSwitch(initialIsOn: Boolean) {
var isOn by remember { mutableStateOf(initialIsOn) }
val touch by animateFloatAsState(
targetValue = if (isOn) 0f else 1f,
animationSpec = spring(
stiffness = Spring.StiffnessHigh
)
)
MechanicalSwitchBackground(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
isOn = !isOn
},
)
},
isOn = isOn
) {
Canvas(
modifier = Modifier
.clip(RectangleShape)
.fillMaxSize()
) {
val dotDiameter = (size.width - 12.dp.toPx()) / 21
val paddingHeight = dotDiameter * 10
val topOffShadowHeight = lerp(0f, paddingHeight, touch)
// TOP REFLECT - ON
val onReflectTop = lerp(5f, 0f, touch)
drawRect(
brush = Brush.horizontalGradient(
colorStops = arrayOf(
0.0f to Color(0x00410D01),
0.03f to Color(0x0D410D01),
0.45f to Color(0xE68D6859),
0.55f to Color(0xE68D6859),
0.97f to Color(0x0D410D01),
1f to Color(0x00410D01),
),
),
size = Size(size.width, onReflectTop),
topLeft = Offset(0f, 7f)
)
// VERTICAL GRADIENT BOTTOM - OFF
drawRect(
brush = Brush.verticalGradient(
colorStops = arrayOf(
0.0f to Color(0x73DC3C05),
0.4f to Color(0xBF4B1902),
0.8f to Color(0x99030303),
0.9f to Color(0x99030303),
1f to Color(0x59AE0302),
),
endY = topOffShadowHeight,
),
size = Size(size.width - 12.dp.toPx(), topOffShadowHeight),
topLeft = Offset(6.dp.toPx(), 0f)
)
// HORIZONTAL GRADIENT TOP - OFF
drawRect(
brush = Brush.horizontalGradient(
colorStops = arrayOf(
0.0f to Color(0x00410D01),
0.03f to Color(0x0D410D01),
0.05f to Color(0xE62D0E02),
0.15f to Color(0x0D2D0E02),
0.85f to Color(0x0D2D0E02),
0.95f to Color(0xE62D0E02),
0.97f to Color(0x0D410D01),
1f to Color(0x00410D01),
),
),
size = Size(size.width, topOffShadowHeight),
topLeft = Offset(0f, 0f)
)
// FLAT SPOT TOP - OFF
drawRect(
brush = Brush.horizontalGradient(
colorStops = arrayOf(
0.0f to Color(0x00410D01),
0.05f to Color(0x4D8C1903),
0.95f to Color(0x4D8C1903),
1f to Color(0x00410D01),
),
),
size = Size(size.width, 20f),
topLeft = Offset(0f, topOffShadowHeight - 20f)
)
// VERTICAL GRADIENT BOTTOM - ON
val bottomShadowOffset = lerp((dotDiameter * 43), size.height, touch)
drawRect(
brush = Brush.verticalGradient(
colorStops = arrayOf(
0.0f to Color(0xBFDC3C05),
0.4f to Color(0xBF4B1902),
0.8f to Color(0x99030303),
0.85f to Color(0x99030303),
1f to Color(0x8CAE0302),
),
startY = bottomShadowOffset,
endY = size.height,
),
size = Size(size.width - 12.dp.toPx(), size.height - bottomShadowOffset),
topLeft = Offset(6.dp.toPx(), bottomShadowOffset)
)
// HORIZONTAL GRADIENT BOTTOM - ON
drawRect(
brush = Brush.horizontalGradient(
colorStops = arrayOf(
0.0f to Color(0x00410D01),
0.03f to Color(0x0D410D01),
0.05f to Color(0xE62D0E02),
0.15f to Color(0x0D2D0E02),
0.85f to Color(0x0D2D0E02),
0.95f to Color(0xE62D0E02),
0.97f to Color(0x0D410D01),
1f to Color(0x00410D01),
),
),
topLeft = Offset(0f, bottomShadowOffset)
)
// FLAT SPOT BOTTOM - ON
drawRect(
brush = Brush.horizontalGradient(
colorStops = arrayOf(
0.0f to Color(0x00410D01),
0.05f to Color(0xCC8C1903),
0.95f to Color(0xCC8C1903),
1f to Color(0x00410D01),
),
),
size = Size(size.width, 20f),
topLeft = Offset(0f, bottomShadowOffset)
)
// BOTTOM REFLECT - OFF
val offReflectBottom = lerp(0f, 5f, touch)
drawRect(
brush = Brush.horizontalGradient(
colorStops = arrayOf(
0.0f to Color(0x00410D01),
0.03f to Color(0x0D410D01),
0.45f to Color(0x8C282015),
0.55f to Color(0x8C282015),
0.97f to Color(0x0D410D01),
1f to Color(0x00410D01),
),
),
size = Size(size.width, offReflectBottom),
topLeft = Offset(0f, size.height - 16f)
)
}
// DOTS
Canvas(
modifier = Modifier
.padding(6.dp)
.clip(RoundedCornerShape(6.dp))
.fillMaxSize()
) {
val dotDiameter = (size.width / 21)
val radius = dotDiameter / 2
val paddingHeight = dotDiameter * 9
var heightCenter = lerp(radius, radius + paddingHeight, touch)
repeat(42) { indexRow ->
repeat(22) { indexColumn ->
val widthCenter = dotDiameter * indexColumn
drawCircle(
brush = Brush.verticalGradient(
colorStops = getDotColor(indexRow, 43, isOn),
startY = heightCenter - radius,
endY = heightCenter + radius,
),
radius = radius,
center = Offset(widthCenter, heightCenter)
)
}
heightCenter += dotDiameter
}
}
}
}
fun getDotColor(
rowIndex: Int,
rowSize: Int,
isOn: Boolean
): Array<Pair<Float, Color>> {
val colorStartOn = Color(0xFFF01E1E)
val colorMiddleOn = Color(0xFFF5F51E)
val colorEndOn = Color(0xFFF5F0F0)
val colorStartOff = Color(0xFF525050)
val colorMiddleOff = Color(0xFF590E0E)
val colorEndOff = Color(0xFF070505)
val colorStart = if (isOn) {
colorStartOn
} else {
colorStartOff
}
val colorMiddle = if (isOn) {
colorMiddleOn
} else {
colorMiddleOff
}
val colorEnd = if (isOn) {
colorEndOn
} else {
colorEndOff
}
// MANUAL 3 COLORS GRADIENT
val returnColorStart = if (rowIndex < (rowSize / 2)) {
val frac = rowIndex.toFloat() / (rowSize.toFloat() / 2)
lerp(colorStart, colorMiddle, frac)
} else {
val startRow = rowIndex - (rowSize / 2)
val frac = (startRow.toFloat() / (rowSize.toFloat() / 2))
lerp(colorMiddle, colorEnd, frac)
}
return arrayOf(
0.0f to returnColorStart.copy(alpha = if (isOn) 0.9f else 0.9f),
0.5f to returnColorStart.copy(alpha = if (isOn) 0.5f else 0.5f),
1.0f to returnColorStart.copy(alpha = if (isOn) 0.1f else 0.1f),
)
}
@Preview
@Composable
fun MechanicalSwitchPreview() {
MechanicalSwitch(true)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment