What You Will Build
A button with a live sine-wave animation drawn on a Canvas. The wave runs continuously across the button surface, and when you press the button the wave amplitude increases for a tactile feel. This creates an organic, fluid effect suitable for music apps, subscription CTAs, or onboarding screens.
Why This Pattern Matters
Canvas drawing in Compose gives you pixel-level control that standard composables cannot match. The liquid button teaches you how to combine rememberInfiniteTransition for perpetual phase animation with touch-driven animateFloatAsState for interactive feedback.
Key Compose Concepts
- Canvas composable for custom drawing
- Path API with sine wave math
- detectTapGestures for press/release interaction
- rememberTextMeasurer to draw text on Canvas
The Liquid Button
@Composable
fun LiquidButton(text: String, color1: Color, color2: Color) {
var isPressed by remember { mutableStateOf(false) }
val infiniteTransition = rememberInfiniteTransition(label = "wave")
val wavePhase by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = (2 * Math.PI).toFloat(),
animationSpec = infiniteRepeatable(
tween(2000, easing = LinearEasing)
),
label = "phase"
)
val waveAmplitude by animateFloatAsState(
targetValue = if (isPressed) 8f else 3f,
animationSpec = spring(dampingRatio = 0.5f),
label = "amp"
)
val textMeasurer = rememberTextMeasurer()
Canvas(
modifier = Modifier
.width(220.dp)
.height(56.dp)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
isPressed = true
tryAwaitRelease()
isPressed = false
}
)
}
) {
val w = size.width
val h = size.height
// Main rounded rectangle
drawRoundRect(
color = color1,
cornerRadius = CornerRadius(28.dp.toPx()),
size = size
)
// Wave overlay using Path
val path = Path().apply {
moveTo(0f, h * 0.5f)
var x = 0f
while (x <= w) {
val y = h * 0.5f +
sin(x * 0.03f + wavePhase) *
waveAmplitude.dp.toPx()
lineTo(x, y)
x += 2f
}
lineTo(w, h)
lineTo(0f, h)
close()
}
drawPath(path, color = color2)
// Centered text
val textLayout = textMeasurer.measure(
text,
style = TextStyle(
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
)
drawText(
textLayout,
topLeft = Offset(
(w - textLayout.size.width) / 2f,
(h - textLayout.size.height) / 2f
)
)
}
}
Tips and Pitfalls
- Wave frequency: The
0.03fmultiplier on x controls how many wave peaks appear. Higher values produce tighter waves. - Use tryAwaitRelease() inside
onPressto properly track press-and-release cycles. - rememberTextMeasurer is essential for drawing text on Canvas with proper layout measurement.
- Step size of 2f in the while loop balances visual smoothness against draw call count.