Una librería de componentes de UI altamente personalizables para Jetpack Compose, construida sobre Material 3. Ofrece opciones de personalización avanzadas (tamaños, colores, formas, tint selectivo por capa) que van más allá de las configuraciones estándar de Material 3.
- ✨ Características
- 📦 Componentes Disponibles
- 🎨 Sistema de Colores
- 🧪 Tests
- 📁 Estructura del Proyecto
- 🚀 Instalación
- 📋 Requisitos
- 🤝 Contribuciones
- 📄 Licencia
- 🎨 Personalización avanzada: Control total sobre colores, tamaños y formas
- 🧩 Basado en Material 3: Integración nativa con el sistema de diseño de Material
- ⚡ Fácil de usar: API intuitiva y compatible con los componentes existentes
- 🖌️ Tinte selectivo por capa (
tintCapytintStroke): Pinta solo las capas que quieras de unImageVector(relleno y/o trazo) y preserva el resto - 🧪 Cubierto por tests: Suite de tests unitarios (JVM) e instrumentados (Compose UI tests)
- 📱 Compatible con API 24+: Soporte para una amplia gama de dispositivos
- 🚀 Release automatizado: Pipeline de CI que publica AAR + release + JitPack al mergear a
master
Un deslizador altamente personalizable con control granular sobre su apariencia.
Propiedades personalizables:
| Propiedad | Tipo | Descripción |
|---|---|---|
thumbSize |
DpSize |
Tamaño del pulgar del slider |
trackHeight |
Dp |
Altura de la pista |
tickSize |
Dp |
Tamaño de las marcas de paso |
colors |
SliderColorsDefaults |
Colores personalizados para cada estado |
Ejemplo de uso:
var sliderValue by remember { mutableFloatStateOf(0.5f) }
SliderComponent(
value = sliderValue,
onValueChange = { sliderValue = it },
thumbSize = DpSize(24.dp, 24.dp),
trackHeight = 12.dp,
tickSize = 8.dp,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.primary,
activeTrackColor = MaterialTheme.colorScheme.primaryContainer,
inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant
),
steps = 4
)Una barra de progreso lineal con mayor control visual y soporte para rangos personalizados.
Propiedades personalizables:
| Propiedad | Tipo | Descripción |
|---|---|---|
width |
Dp |
Ancho del indicador |
height |
Dp |
Alto del indicador |
range |
ClosedFloatingPointRange<Float> |
Rango de valores (ej: 0f..100f) |
strokeCap |
StrokeCap |
Estilo de los extremos de la línea |
gapSize |
Dp |
Espacio entre indicador y pista |
drawStopIndicator |
DrawScope.() -> Unit |
Indicador de parada personalizado |
Ejemplo de uso:
LinearProgressIndicatorComponents(
progress = { 0.7f },
width = 280.dp,
height = 12.dp,
range = 0f..1f,
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.surfaceVariant,
strokeCap = StrokeCap.Round,
gapSize = 4.dp
)Un deslizador de rango para seleccionar intervalos de valores, con consistencia visual respecto a los demás componentes.
Propiedades personalizables:
| Propiedad | Tipo | Descripción |
|---|---|---|
state |
RangeSliderState |
Estado del slider de rango |
thumbSize |
DpSize |
Tamaño de ambos pulgares |
trackHeight |
Dp |
Altura de la pista |
tickSize |
Dp |
Tamaño de las marcas |
startThumb / endThumb |
@Composable |
Pulgares personalizados |
track |
@Composable |
Pista personalizada |
Ejemplo de uso:
val rangeSliderState = remember {
RangeSliderState(
activeRangeStart = 0.2f,
activeRangeEnd = 0.8f,
steps = 3
)
}
RangeSliderComponent(
state = rangeSliderState,
thumbSize = DpSize(20.dp, 20.dp),
trackHeight = 10.dp,
tickSize = 6.dp,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.primary,
activeTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),
inactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerLow
)
)Wrapper sobre androidx.compose.material3.Icon que añade el parámetro tintCap para controlar qué capas (layers) de un ImageVector reciben el color de tint. Las capas no afectadas conservan sus colores originales.
💡 ¿Por qué? Cuando tiñes un
ImageVectorcomplejo (logos, ilustraciones, íconos con partes de marca) normalmente todo el vector se vuelve del color deltint. ContintCappuedes pintar solo las capas que sí deben cambiar de color y dejar intactas las que representan la identidad visual (p.ej. el fondo o un detalle de marca).
Propiedades personalizables:
| Propiedad | Tipo | Descripción |
|---|---|---|
imageVector |
ImageVector |
Vector a renderizar |
contentDescription |
String? |
Descripción para accesibilidad |
modifier |
Modifier |
Modificador estándar |
tint |
Color |
Color a aplicar (por defecto LocalContentColor.current) |
tintCap |
TintCap |
Alcance del tint del relleno (ver tabla abajo, por defecto TintCap.All) |
tintStroke |
Color? |
Color del trazo a aplicar (opcional, por defecto null) |
tintStrokeCap |
TintStroke |
Alcance del tint del trazo (por defecto TintStroke.All) |
Variantes de TintCap:
| Variante | Descripción |
|---|---|
TintCap.All |
Pinta todas las capas con tint (default para Icon, equivale al comportamiento estándar de Compose) |
TintCap.Undefined |
No aplica ninguna transformación; el vector se renderiza con sus colores originales |
TintCap.index(n) |
Pinta solo la capa top-level en el índice n |
TintCap.range(rango) |
Pinta todas las capas cuyo índice esté dentro de rango (ej: 0..2) |
TintCap.layers(1, 3) |
Pinta solo las capas top-level en los índices indicados |
Una "capa" es cada nodo de primer nivel del
ImageVectorraíz (ya sea unVectorGroupo unVectorPathdirecto). Si la capa es un grupo, todo su contenido se pinta con el mismo criterio.
Ejemplos de uso:
// Default: pinta todas las capas
IconComponents(
imageVector = Icons.Filled.Favorite,
contentDescription = null,
tint = Color.Red
)
// Pinta solo la capa top-level en el índice 1
IconComponents(
imageVector = Icons.Filled.Favorite,
contentDescription = null,
tint = Color.Red,
tintCap = TintCap.index(1)
)
// Pinta el rango 0..2 y respeta el resto
IconComponents(
imageVector = Icons.Filled.Favorite,
contentDescription = null,
tint = Color.Red,
tintCap = TintCap.range(0..2)
)
// Pinta múltiples capas específicas
IconComponents(
imageVector = Icons.Filled.Favorite,
contentDescription = null,
tint = Color.Red,
tintCap = TintCap.layers(1, 3)
)
// Respeta los colores originales del vector ignorando tint
IconComponents(
imageVector = Icons.Filled.Favorite,
contentDescription = null,
tint = Color.Red, // se ignora por estar Undefined
tintCap = TintCap.Undefined
)El módulo incluye un ImageVector de camión multi-capa pensado para ejercitar tintCap:
Índice 0 → wheels (grupo con 2 neumáticos) #424242
Índice 1 → body (cama del camión) #E53935
Índice 2 → cab (cabina + ventana) #1E88E5
Índice 3 → cargo (caja de carga) #43A047
Cada capa además tiene un trazo por defecto con color distintivo para que puedas ejercitar tintStroke:
Índice 0 → wheels (trazo) #212121
Índice 1 → body (trazo) #B71C1C
Índice 2 → cab (trazo) #0D47A1
Índice 3 → cargo (trazo) #1B5E20
Úsalo para prototipar y validar el comportamiento de tintCap y tintStroke sin necesidad de un asset externo:
IconComponents(
imageVector = Icons.MapTruck,
contentDescription = "Truck",
tint = Color.Yellow,
tintCap = TintCap.layers(0, 3), // solo neumáticos y carga en amarillo
tintStroke = Color(0xFF00BCD4), // trazo cyan en todas las capas
tintStrokeCap = TintStroke.All
)Además de controlar el color del relleno con tintCap, IconComponents admite tintStroke para controlar el color del trazo (stroke) de cada capa de forma independiente.
tintStroke: Color?— color del trazo. Cuando esnull(default) no se transforma el trazo del vector.tintStrokeCap: TintStroke— qué capas reciben el color del trazo, con las mismas variantes queTintCap:Variante Descripción TintStroke.AllRecolorrea todas las capas (default) TintStroke.UndefinedNo aplica ninguna transformación; el trazo se conserva TintStroke.index(n)Recolorrea solo la capa top-level en el índice nTintStroke.range(rango)Recolorrea todas las capas cuyo índice esté dentro del rango TintStroke.layers(1, 3)Recolorrea solo las capas top-level en los índices indicados
💡 ¿Por qué? Un mismo vector puede tener relleno de marca (que quieres preservar) y trazos que sí deben personalizarse (color de acento, modo oscuro, estados hover, etc.). Con
tintStroke/tintStrokeCappuedes controlar el color del trazo capa por capa, igual que contintCap/TintCap.
Ejemplos:
// Recolorear el trazo de TODAS las capas con un color de acento
IconComponents(
imageVector = Icons.MapTruck,
contentDescription = "Truck",
tint = Color.Yellow,
tintCap = TintCap.Undefined, // relleno intacto
tintStroke = Color(0xFF00BCD4), // trazo cyan
tintStrokeCap = TintStroke.All
)
// Cambiar el color del trazo SOLO de los neumáticos y la carga
IconComponents(
imageVector = Icons.MapTruck,
contentDescription = "Truck",
tintStroke = Color.Red,
tintStrokeCap = TintStroke.layers(0, 3)
)
// Combinar relleno y trazo sobre capas diferentes
IconComponents(
imageVector = Icons.MapTruck,
contentDescription = "Truck",
tint = Color(0xFF4CAF50), // relleno verde solo en ruedas y carga
tintCap = TintCap.layers(0, 3),
tintStroke = Color(0xFFFF9800), // trazo naranja solo en chasis y cabina
tintStrokeCap = TintStroke.layers(1, 2)
)Wrapper sobre androidx.compose.foundation.Image con la misma potencia de tintCap que IconComponents. Pensado para vectores con varias capas donde queremos preservar colores originales (logos, ilustraciones, etc.).
Propiedades personalizables:
| Propiedad | Tipo | Descripción |
|---|---|---|
imageVector |
ImageVector |
Vector a renderizar |
contentDescription |
String? |
Descripción para accesibilidad |
modifier |
Modifier |
Modificador estándar |
alignment |
Alignment |
Alineación dentro del espacio disponible |
contentScale |
ContentScale |
Estrategia de escalado (default ContentScale.Fit) |
alpha |
Float |
Opacidad (default DefaultAlpha) |
colorFilter |
ColorFilter? |
Filtro de color opcional adicional |
tint |
Color? |
Color a aplicar (opcional) |
tintCap |
TintCap |
Alcance del tint del relleno (default TintCap.Undefined) |
tintStroke |
Color? |
Color del trazo a aplicar (opcional) |
tintStrokeCap |
TintStroke |
Alcance del tint del trazo (default TintStroke.All) |
Ejemplo de uso:
// Logo con fondo original y un solo trazo tintado
ImageComponents(
imageVector = myBrandLogo,
contentDescription = "Logo",
modifier = Modifier.size(120.dp),
tint = MaterialTheme.colorScheme.primary,
tintCap = TintCap.index(0)
)
// Todas las capas pintadas con tint
ImageComponents(
imageVector = myBrandLogo,
contentDescription = "Logo",
modifier = Modifier.size(120.dp),
tint = MaterialTheme.colorScheme.primary,
tintCap = TintCap.All
)
// Colores originales del vector intactos (sin transformación)
ImageComponents(
imageVector = myBrandLogo,
contentDescription = "Logo",
modifier = Modifier.size(120.dp),
tintCap = TintCap.Undefined
)Además de controlar el color del relleno con tintCap, los componentes IconComponents e ImageComponents admiten tintStroke para controlar el color del trazo (stroke) de cada capa de forma independiente.
tintStroke: Color?— color del trazo. Cuando esnull(default) no se transforma el trazo del vector.tintStrokeCap: TintStroke— qué capas reciben el color del trazo, con las mismas variantes queTintCap:Variante Descripción TintStroke.AllRecolorrea todas las capas (default) TintStroke.UndefinedNo aplica ninguna transformación; el trazo se conserva TintStroke.index(n)Recolorrea solo la capa top-level en el índice nTintStroke.range(rango)Recolorrea todas las capas cuyo índice esté dentro del rango TintStroke.layers(1, 3)Recolorrea solo las capas top-level en los índices indicados
💡 ¿Por qué? Un mismo vector puede tener relleno de marca (que quieres preservar) y trazos que sí deben personalizarse (color de acento, modo oscuro, estados hover, etc.). Con
tintStroke/tintStrokeCappuedes controlar el color del trazo capa por capa, igual que contintCap/TintCap.
Ejemplos:
// Recolorear el trazo de TODAS las capas con un color de acento
IconComponents(
imageVector = Icons.MapTruck,
contentDescription = "Truck",
tint = Color.Yellow,
tintCap = TintCap.Undefined, // relleno intacto
tintStroke = Color(0xFF00BCD4), // trazo cyan
tintStrokeCap = TintStroke.All
)
// Cambiar el color del trazo SOLO de los neumáticos y la carga
IconComponents(
imageVector = Icons.MapTruck,
contentDescription = "Truck",
tintStroke = Color.Red,
tintStrokeCap = TintStroke.layers(0, 3)
)
// Combinar relleno y trazo sobre capas diferentes
IconComponents(
imageVector = Icons.MapTruck,
contentDescription = "Truck",
tint = Color(0xFF4CAF50), // relleno verde solo en ruedas y carga
tintCap = TintCap.layers(0, 3),
tintStroke = Color(0xFFFF9800), // trazo naranja solo en chasis y cabina
tintStrokeCap = TintStroke.layers(1, 2)
)Todos los componentes utilizan SliderColorsDefaults para una gestión coherente de colores:
SliderColorsDefaults(
thumbColor = Color, // Color del pulgar
activeTrackColor = Color, // Color de la pista activa
activeTickColor = Color, // Color de las marcas activas
inactiveTrackColor = Color, // Color de la pista inactiva
inactiveTickColor = Color, // Color de las marcas inactivas
disabledThumbColor = Color, // Color del pulgar deshabilitado
disabledActiveTrackColor = Color,
disabledActiveTickColor = Color,
disabledInactiveTrackColor = Color,
disabledInactiveTickColor = Color
)Cada componente está cubierto por tests. Para ejecutarlos:
# Tests unitarios (JVM) — rápidos, no requieren emulador
./gradlew :component:testDebugUnitTest
# Tests instrumentados (Compose UI tests) — requieren emulador o dispositivo
./gradlew :app:connectedDebugAndroidTestCobertura:
| Componente | Unit tests | Instrumented UI tests |
|---|---|---|
SliderComponent |
— | — |
LinearProgressIndicatorComponents |
— | — |
RangeSliderComponent |
— | — |
TintCap |
✅ 9 tests | ✅ vía Icon / Image |
TintStroke |
✅ 9 tests | ✅ vía Icon / Image |
ImageVectorTinter |
✅ 7 tests + 6 stroke | ✅ vía Icon / Image |
IconComponents (con tintCap / tintStroke) |
— | ✅ 6 tests + 7 stroke |
ImageComponents (con tintCap / tintStroke) |
— | ✅ 5 tests + 6 stroke |
Los UI tests renderizan el fixture Icons.MapTruck (4 capas top-level con colores distinguibles) y muestrean píxeles del bitmap capturado para verificar que cada variante de tintCap pinta exactamente las capas correctas.
composecomponents/
├── app/ # Aplicación de demostración
│ └── src/main/java/com/blipblipcode/compose_components/
│ └── MainActivity.kt # Incluye el fixture Icons.MapTruck
├── component/ # Módulo de la librería
│ └── src/main/java/com/blipblipcode/component/
│ ├── slider/ # SliderComponent y utilidades
│ │ ├── SliderComponent.kt
│ │ ├── SliderDefaults.kt
│ │ ├── SliderColorsDefaults.kt
│ │ └── SliderSizeDefaults.kt
│ ├── linear/ # LinearProgressIndicatorComponents
│ │ └── LinearProgressIndicatorComponents.kt
│ ├── range/ # RangeSliderComponent
│ │ ├── RangeSliderComponent.kt
│ │ └── RangeSliderDefaults.kt
│ └── image/ # IconComponents e ImageComponents con tintCap
│ ├── TintCap.kt # Sealed class (All / Undefined / Index / Range / Layers)
│ ├── ImageVectorTinter.kt # Lógica interna de re-tintado selectivo
│ ├── Icon.kt # Wrapper de Material3 Icon → IconComponents
│ ├── Image.kt # Wrapper de Foundation Image → ImageComponents
│ └── MapTruck.kt # Fixture ImageVector de 4 capas
│ └── src/test/ # Tests unitarios (JVM)
│ └── java/com/blipblipcode/component/image/
│ ├── TintCapTest.kt # 9 tests
│ └── ImageVectorTinterTest.kt # 7 tests
│ └── src/androidTest/ # Tests instrumentados (Compose UI)
│ └── java/com/blipblipcode/component/image/
│ ├── IconTintCapTest.kt # 6 tests
│ └── ImageTintCapTest.kt # 5 tests
├── .github/workflows/
│ ├── workflows/
│ │ ├── ci.yml # CI: tests on every PR to master (open/synchronize)
│ │ └── release.yml # Release: build AAR + tag + GitHub Release + JitPack on PR close
│ ├── CODEOWNERS # Code owners del repo (para branch protection)
│ └── branch-protection/
│ └── master.json # Config de protección aplicada a master (reproducible vía gh api)
└── gradle/
└── libs.versions.toml # Catálogo de versiones
Cada merge a master publica automáticamente un nuevo tag + AAR en GitHub Releases y dispara una build en JitPack.
Agrega el repositorio de JitPack en tu settings.gradle.kts:
dependencyResolutionManagement {
repositories {
maven { url = uri("https://jitpack.io") }
}
}Y luego la dependencia en el módulo de tu app:
dependencies {
implementation("com.github.LeandroLCD:compose-components:<TAG>")
}Reemplaza <TAG> por el tag publicado (ej: v0.1.0). Los tags y el changelog están en la pestaña Releases del repositorio.
⚠️ La primera vez que importes el tag, JitPack necesita compilar el módulo; puede tardar unos minutos. Builds subsiguientes son instantáneas.
Incluye el módulo :component en tus dependencias de Gradle:
dependencies {
implementation(project(":component"))
}Asegúrate de tener habilitado Compose en tu build.gradle.kts:
android {
buildFeatures {
compose = true
}
}| Requisito | Versión mínima |
|---|---|
| Android Studio | Ladybug o superior |
| Kotlin | 2.3.10+ |
| Compose BOM | 2026.02.00+ |
| Min SDK | 24 (Android 7.0) |
| Target SDK | 36 |
| JVM Target | 17 |
| AGP | 9.0.0+ |
¡Las contribuciones son bienvenidas! Si deseas contribuir:
- Haz un Fork del proyecto
- Crea una rama desde
masterpara tu feature (git checkout -b feature/nueva-funcionalidad) - Realiza tus cambios y haz commit (
git commit -m 'feat: añade nueva funcionalidad') - Push a la rama (
git push origin feature/nueva-funcionalidad) - Abre un Pull Request hacia
master
El pipeline de CI correrá tests unitarios + instrumentados (API 36) y, al mergear, publicará un nuevo release.
La rama master está protegida y solo recibe cambios vía Pull Request:
- ✅ Pull request obligatorio antes de mergear
- ✅ 1 aprobación de code review
- ✅ Revisión de code owner requerida (definido en
.github/CODEOWNERS) - ✅ Reviews stale se descartan ante nuevos pushes
- ✅ Historial lineal (squash o rebase — no merge commits)
- ✅ Force-push y borrado de rama deshabilitados
- ✅ Conversaciones sin resolver bloquean el merge
- ✅ Reglas aplicadas incluso a administradores (
enforce_admins: true)
Este proyecto está bajo la Licencia MIT. Consulta el archivo LICENSE para más detalles.
Si tienes preguntas o sugerencias, no dudes en abrir un Issue en el repositorio.
Hecho con ❤️ usando Jetpack Compose y Material 3