Canvas básico
// Canvas se comporta como cualquier composable — tiene tamaño y se integra en layouts
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
) {
// this: DrawScope — acceso a size, center, drawCircle, drawLine, etc.
// size.width y size.height en píxeles (ya convertidos de Dp)
val centroX = size.width / 2
val centroY = size.height / 2
// Dibujar un círculo
drawCircle(
color = Color.Blue,
radius = 50.dp.toPx(),
center = Offset(centroX, centroY)
)
// Dibujar una línea
drawLine(
color = Color.Red,
start = Offset(0f, 0f),
end = Offset(size.width, size.height),
strokeWidth = 2.dp.toPx()
)
// Dibujar un rectángulo
drawRect(
color = Color.Green.copy(alpha = 0.3f),
topLeft = Offset(20.dp.toPx(), 20.dp.toPx()),
size = Size(100.dp.toPx(), 60.dp.toPx())
)
}
// Un indicador de progreso en arco
@Composable
fun IndicadorArco(
progreso: Float, // 0f a 1f
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.primary,
trackColor: Color = MaterialTheme.colorScheme.surfaceVariant
) {
Canvas(modifier = modifier.size(120.dp)) {
val strokeWidth = 12.dp.toPx()
val radio = (size.minDimension - strokeWidth) / 2
val centroX = size.width / 2
val centroY = size.height / 2
// Track de fondo (arco completo gris)
drawArc(
color = trackColor,
startAngle = 135f,
sweepAngle = 270f,
useCenter = false,
topLeft = Offset(centroX - radio, centroY - radio),
size = Size(radio * 2, radio * 2),
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
// Progreso (arco proporcional al progreso)
drawArc(
color = color,
startAngle = 135f,
sweepAngle = 270f * progreso,
useCenter = false,
topLeft = Offset(centroX - radio, centroY - radio),
size = Size(radio * 2, radio * 2),
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
}
}
// Path personalizado — forma de diamante
@Composable
fun Diamante(color: Color, modifier: Modifier = Modifier) {
Canvas(modifier = modifier.size(80.dp)) {
val path = Path().apply {
moveTo(size.width / 2, 0f) // arriba
lineTo(size.width, size.height / 2) // derecha
lineTo(size.width / 2, size.height) // abajo
lineTo(0f, size.height / 2) // izquierda
close()
}
drawPath(path = path, color = color)
}
}
Gradientes
Canvas(modifier = Modifier.fillMaxWidth().height(150.dp)) {
// Gradiente lineal horizontal
val gradienteHorizontal = Brush.horizontalGradient(
colors = listOf(Color.Blue, Color.Cyan, Color.Green)
)
drawRect(brush = gradienteHorizontal, size = size)
// Gradiente radial
val gradienteRadial = Brush.radialGradient(
colors = listOf(Color.Yellow, Color.Transparent),
center = center,
radius = size.minDimension / 2
)
drawCircle(brush = gradienteRadial, radius = size.minDimension / 2)
// Gradiente en un path
val gradientePath = Brush.linearGradient(
0.0f to Color.Red,
0.5f to Color.Magenta,
1.0f to Color.Blue,
start = Offset(0f, 0f),
end = Offset(size.width, size.height)
)
drawRoundRect(
brush = gradientePath,
cornerRadius = CornerRadius(16.dp.toPx()),
size = Size(size.width * 0.8f, size.height * 0.6f),
topLeft = Offset(size.width * 0.1f, size.height * 0.2f)
)
}
Texto en Canvas
@Composable
fun CanvasConTexto() {
// Para texto en Canvas necesitamos TextMeasurer
val textMeasurer = rememberTextMeasurer()
Canvas(modifier = Modifier.fillMaxWidth().height(100.dp)) {
val textResult = textMeasurer.measure(
text = "Compose Canvas",
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
)
// Centrar el texto
val textoX = (size.width - textResult.size.width) / 2
val textoY = (size.height - textResult.size.height) / 2
// Fondo
drawRoundRect(
color = Color.DarkGray,
size = size,
cornerRadius = CornerRadius(8.dp.toPx())
)
// Texto
drawText(
textLayoutResult = textResult,
topLeft = Offset(textoX, textoY)
)
}
}
Canvas(modifier = Modifier.size(200.dp)) {
// Rotar alrededor del centro
rotate(degrees = 45f, pivot = center) {
drawRect(color = Color.Blue, size = Size(100.dp.toPx(), 20.dp.toPx()),
topLeft = Offset(center.x - 50.dp.toPx(), center.y - 10.dp.toPx()))
}
// Escalar desde el centro
scale(scaleX = 1.5f, scaleY = 0.8f, pivot = center) {
drawCircle(color = Color.Red.copy(alpha = 0.5f), radius = 30.dp.toPx())
}
// Transladar (mover el origen)
translate(left = 50.dp.toPx(), top = 20.dp.toPx()) {
drawRect(color = Color.Green, size = Size(40.dp.toPx(), 40.dp.toPx()))
}
// Apilar transformaciones
withTransform({
translate(left = size.width / 2, top = size.height / 2)
rotate(degrees = 30f)
scale(0.8f)
}) {
drawRect(color = Color.Magenta, size = Size(60.dp.toPx(), 60.dp.toPx()),
topLeft = Offset(-30.dp.toPx(), -30.dp.toPx()))
}
}
Canvas animado
// Indicador de carga pulsante
@Composable
fun PulsingLoader(color: Color = MaterialTheme.colorScheme.primary) {
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
val escala by infiniteTransition.animateFloat(
initialValue = 0.6f,
targetValue = 1.0f,
animationSpec = infiniteRepeatable(
animation = tween(800, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "escala"
)
val alpha by infiniteTransition.animateFloat(
initialValue = 0.3f,
targetValue = 1.0f,
animationSpec = infiniteRepeatable(
animation = tween(800),
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
Canvas(modifier = Modifier.size(60.dp)) {
drawCircle(
color = color.copy(alpha = alpha),
radius = (size.minDimension / 2) * escala
)
}
}
// Gráfico de línea animado que se dibuja progresivamente
@Composable
fun LineaAnimada(puntos: List<Float>) {
var progreso by remember { mutableStateOf(0f) }
LaunchedEffect(puntos) {
animate(0f, 1f, animationSpec = tween(1500)) { valor, _ ->
progreso = valor
}
}
Canvas(modifier = Modifier.fillMaxWidth().height(150.dp)) {
if (puntos.isEmpty()) return@Canvas
val puntosADibujar = (puntos.size * progreso).toInt().coerceAtLeast(2)
val pasoX = size.width / (puntos.size - 1)
val maxValor = puntos.max()
val path = Path()
puntos.take(puntosADibujar).forEachIndexed { index, valor ->
val x = index * pasoX
val y = size.height - (valor / maxValor) * size.height
if (index == 0) path.moveTo(x, y) else path.lineTo(x, y)
}
drawPath(path = path, color = Color.Blue,
style = Stroke(width = 3.dp.toPx(), cap = StrokeCap.Round, join = StrokeJoin.Round))
}
}
drawBehind y drawWithContent
// drawBehind: dibujar detrás del contenido del composable
// Más eficiente que Canvas para decoraciones simples
Box(
modifier = Modifier
.padding(16.dp)
.drawBehind {
// Sombra custom
drawRoundRect(
color = Color.Black.copy(alpha = 0.15f),
topLeft = Offset(4.dp.toPx(), 4.dp.toPx()),
size = Size(size.width, size.height),
cornerRadius = CornerRadius(8.dp.toPx())
)
// Fondo de la card
drawRoundRect(
color = Color.White,
size = size,
cornerRadius = CornerRadius(8.dp.toPx())
)
}
) {
Text("Contenido con sombra custom", modifier = Modifier.padding(16.dp))
}
// drawWithContent: dibujar antes Y después del contenido
Text(
text = "Texto con subrayado animado",
modifier = Modifier.drawWithContent {
drawContent() // primero el texto
// Luego el subrayado encima
drawLine(
color = Color.Blue,
start = Offset(0f, size.height),
end = Offset(size.width, size.height),
strokeWidth = 2.dp.toPx()
)
}
)