Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c6d7056
feat: add tintCap support for Icon and Image components
LeandroLCD Aug 14, 2026
f722b79
ci: add release pipeline and fix artifactId
LeandroLCD Aug 14, 2026
7efa511
fix(tintCap): resolve fixture overlaps and stabilise UI tests
LeandroLCD Aug 14, 2026
a9d76b1
refactor(image): rename Icon → IconComponents and Image → ImageCompon…
LeandroLCD Aug 14, 2026
4be663a
test(ui): add instrumented UI tests for Linear, Slider and RangeSlider
LeandroLCD Aug 14, 2026
034af79
ci: split pipeline into ci + release and protect master
LeandroLCD Aug 14, 2026
7f13738
remove branch protection configuration and update README documentation
LeandroLCD Aug 14, 2026
32cb4d2
ci: unify Gradle cache key across ci + release workflows
LeandroLCD Aug 14, 2026
9296be0
add pipeline discord notification
LeandroLCD Aug 14, 2026
d32d7a6
chore: bump version to 0.1.1
LeandroLCD Aug 14, 2026
b4c9d27
Merge branch 'master' into develop
LeandroLCD Aug 14, 2026
d8a76c5
ci: add branch argument to release workflow
LeandroLCD Aug 14, 2026
d87e082
Merge remote-tracking branch 'origin/develop' into develop
LeandroLCD Aug 14, 2026
f9049e6
Merge branch 'master' into develop
LeandroLCD Aug 14, 2026
b5063ae
chore: update version to 0.1.11
LeandroLCD Aug 14, 2026
a2f5fbd
chore: simplify Gradle cache configuration and standardize keys in Gi…
LeandroLCD Aug 14, 2026
b66e86f
chore: simplify Gradle cache configuration and standardize keys in Gi…
LeandroLCD Aug 14, 2026
e5e41e5
feat: add `tintStroke` support to `IconComponents` and `ImageComponents`
LeandroLCD Aug 14, 2026
221c7de
chore: suppress KDocUnresolvedReference warning in ImageVectorTinterTest
LeandroLCD Aug 14, 2026
79fd466
feat: add TintStroke for selective layer stroke tinting in IconCompon…
LeandroLCD Aug 14, 2026
bd47092
chore: upgrade version to 0.1.20
LeandroLCD Aug 14, 2026
d381c23
Merge branch 'master' into develop
LeandroLCD Aug 14, 2026
1ac3f16
chore: upgrade version to 0.1.20
LeandroLCD Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 120 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Una librería de componentes de UI altamente personalizables para **Jetpack Comp
- 🎨 **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 (`tintCap`)**: Pinta solo las capas que quieras de un `ImageVector` y preserva el resto
- 🖌️ **Tinte selectivo por capa (`tintCap` y `tintStroke`)**: Pinta solo las capas que quieras de un `ImageVector` (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`
Expand Down Expand Up @@ -162,7 +162,9 @@ Wrapper sobre `androidx.compose.material3.Icon` que añade el parámetro `tintCa
| `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 (ver tabla abajo, por defecto `TintCap.All`) |
| `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 |
Expand Down Expand Up @@ -229,14 +231,73 @@ El módulo incluye un `ImageVector` de camión multi-capa pensado para ejercitar
Índice 3 → cargo (caja de carga) #43A047
```

Úsalo para prototipar y validar el comportamiento de `tintCap` sin necesidad de un asset externo:
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:

```kotlin
IconComponents(
imageVector = Icons.MapTruck,
contentDescription = "Truck",
tint = Color.Yellow,
tintCap = TintCap.layers(0, 3) // solo neumáticos y carga en amarillo
tintCap = TintCap.layers(0, 3), // solo neumáticos y carga en amarillo
tintStroke = Color(0xFF00BCD4), // trazo cyan en todas las capas
tintStrokeCap = TintStroke.All
)
```

#### Tinte selectivo de **trazo** (`tintStroke` + `tintStrokeCap`)

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 es `null` (default) no se transforma el trazo del vector.
- `tintStrokeCap: TintStroke` — qué capas reciben el color del trazo, con las mismas variantes que `TintCap`:
| Variante | Descripción |
|----------|-------------|
| `TintStroke.All` | Recolorrea **todas** las capas (default) |
| `TintStroke.Undefined` | **No aplica** ninguna transformación; el trazo se conserva |
| `TintStroke.index(n)` | Recolorrea **solo** la capa top-level en el índice `n` |
| `TintStroke.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`/`tintStrokeCap` puedes controlar el color del trazo capa por capa, igual que con `tintCap`/`TintCap`.

**Ejemplos:**

```kotlin
// 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)
)
```

Expand All @@ -257,7 +318,9 @@ Wrapper sobre `androidx.compose.foundation.Image` con la misma potencia de `tint
| `alpha` | `Float` | Opacidad (default `DefaultAlpha`) |
| `colorFilter` | `ColorFilter?` | Filtro de color opcional adicional |
| `tint` | `Color?` | Color a aplicar (opcional) |
| `tintCap` | `TintCap` | Alcance del tint (default `TintCap.Undefined`) |
| `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:**

Expand Down Expand Up @@ -289,6 +352,54 @@ ImageComponents(
)
```

#### Tinte selectivo de **trazo** (`tintStroke` + `tintStrokeCap`)

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 es `null` (default) no se transforma el trazo del vector.
- `tintStrokeCap: TintStroke` — qué capas reciben el color del trazo, con las mismas variantes que `TintCap`:
| Variante | Descripción |
|----------|-------------|
| `TintStroke.All` | Recolorrea **todas** las capas (default) |
| `TintStroke.Undefined` | **No aplica** ninguna transformación; el trazo se conserva |
| `TintStroke.index(n)` | Recolorrea **solo** la capa top-level en el índice `n` |
| `TintStroke.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`/`tintStrokeCap` puedes controlar el color del trazo capa por capa, igual que con `tintCap`/`TintCap`.

**Ejemplos:**

```kotlin
// 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)
)
```

---

## 🎨 Sistema de Colores
Expand Down Expand Up @@ -332,9 +443,10 @@ Cada componente está cubierto por tests. Para ejecutarlos:
| `LinearProgressIndicatorComponents` | — | — |
| `RangeSliderComponent` | — | — |
| `TintCap` | ✅ 9 tests | ✅ vía `Icon` / `Image` |
| `ImageVectorTinter` | ✅ 7 tests | ✅ vía `Icon` / `Image` |
| `IconComponents` (con `tintCap`) | — | ✅ 6 tests |
| `ImageComponents` (con `tintCap`) | — | ✅ 5 tests |
| `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.

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ plugins {
alias(libs.plugins.android.library) apply false
}

version = "0.1.11"
version = "0.1.20"
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package com.blipblipcode.component.image

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.dp
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test

/**
* Instrumented UI tests for [IconComponents] with the [tintStroke] parameter using the
* [Icons.MapTruck] fixture. Each layer has a distinctive default stroke colour so we can
* verify that only the layers targeted by [tintStrokeCap] receive the new stroke colour.
*/
class IconTintStrokeTest {

@get:Rule
val composeTestRule = createComposeRule()

private val testTagValue = "icon-stroke-under-test"
private val iconSizeDp = 128.dp

private val strokeTintColor = Color(0xFFFF00FF) // magenta

// Default stroke colours of Icons.MapTruck — see MapTruck.kt
private val wheelsStrokeColor = Color(0xFF212121)
private val bodyStrokeColor = Color(0xFFB71C1C)
private val cabStrokeColor = Color(0xFF0D47A1)
private val cargoStrokeColor = Color(0xFF1B5E20)

/**
* Renders the truck wrapped in an [IconComponents] with the given stroke configuration
* and samples pixels at well-known stroke positions.
*
* Coordinates are chosen to land exactly on the stroke centre of each layer so the
* captured pixel reflects the stroke colour rather than the fill colour or background.
*/
private fun renderAndSampleStroke(
tint: Color = Color.Unspecified,
tintCap: TintCap = TintCap.Undefined,
strokeTint: Color? = strokeTintColor,
strokeCap: TintStroke = TintStroke.All,
): IntArray {
composeTestRule.setContent {
Surface(modifier = Modifier.background(Color.White)) {
Box(
modifier = Modifier
.size(iconSizeDp)
.background(Color.White)
.testTag(testTagValue)
) {
IconComponents(
imageVector = Icons.MapTruck,
contentDescription = null,
modifier = Modifier.size(iconSizeDp),
tint = tint,
tintCap = tintCap,
tintStroke = strokeTint,
tintStrokeCap = strokeCap,
)
}
}
}
composeTestRule.waitForIdle()
val bmp = composeTestRule.onNodeWithTag(testTagValue).captureToImage().asAndroidBitmap()
val w = bmp.width
val h = bmp.height
return intArrayOf(
bmp.getPixel(w * 17 / 64, h * 54 / 64), // right edge of front tire (wheels stroke)
bmp.getPixel(w * 32 / 64, h * 44 / 64), // top edge of body chassis
bmp.getPixel(w * 51 / 64, h * 14 / 64), // top edge of cab shell
bmp.getPixel(w * 20 / 64, h * 4 / 64) // top edge of cargo
)
}

@Test
fun stroke_null_preserves_original_stroke_colors() {
val px = renderAndSampleStroke(strokeTint = null)
assertEquals(wheelsStrokeColor.toArgb(), px[0])
assertEquals(bodyStrokeColor.toArgb(), px[1])
assertEquals(cabStrokeColor.toArgb(), px[2])
assertEquals(cargoStrokeColor.toArgb(), px[3])
}

@Test
fun stroke_all_recolors_every_layer_stroke() {
val px = renderAndSampleStroke(strokeCap = TintStroke.All)
assertEquals(strokeTintColor.toArgb(), px[0])
assertEquals(strokeTintColor.toArgb(), px[1])
assertEquals(strokeTintColor.toArgb(), px[2])
assertEquals(strokeTintColor.toArgb(), px[3])
}

@Test
fun stroke_index_recolors_only_the_matching_layer_stroke() {
val px = renderAndSampleStroke(strokeCap = TintStroke.index(2))
assertEquals(wheelsStrokeColor.toArgb(), px[0])
assertEquals(bodyStrokeColor.toArgb(), px[1])
assertEquals(strokeTintColor.toArgb(), px[2])
assertEquals(cargoStrokeColor.toArgb(), px[3])
}

@Test
fun stroke_range_recolors_only_layers_inside_the_range() {
val px = renderAndSampleStroke(strokeCap = TintStroke.range(0..1))
assertEquals(strokeTintColor.toArgb(), px[0])
assertEquals(strokeTintColor.toArgb(), px[1])
assertEquals(cabStrokeColor.toArgb(), px[2])
assertEquals(cargoStrokeColor.toArgb(), px[3])
}

@Test
fun stroke_layers_recolors_only_the_specified_positions() {
val px = renderAndSampleStroke(strokeCap = TintStroke.layers(0, 3))
assertEquals(strokeTintColor.toArgb(), px[0])
assertEquals(bodyStrokeColor.toArgb(), px[1])
assertEquals(cabStrokeColor.toArgb(), px[2])
assertEquals(strokeTintColor.toArgb(), px[3])
}

@Test
fun stroke_undefined_preserves_original_stroke_colors_even_when_strokeTint_is_set() {
val px = renderAndSampleStroke(strokeCap = TintStroke.Undefined)
assertEquals(wheelsStrokeColor.toArgb(), px[0])
assertEquals(bodyStrokeColor.toArgb(), px[1])
assertEquals(cabStrokeColor.toArgb(), px[2])
assertEquals(cargoStrokeColor.toArgb(), px[3])
}

@Test
fun fill_and_stroke_tints_can_be_combined_on_different_layers() {
// Fill tint on layers 0 and 3 (wheels + cargo), stroke tint on layers 1 and 2 (body + cab).
val px = renderAndSampleStroke(
tint = Color(0xFF00FF00),
tintCap = TintCap.layers(0, 3),
strokeTint = Color(0xFF00FFFF),
strokeCap = TintStroke.layers(1, 2),
)
// Stroke pixels reflect the new stroke colour on layers 1 and 2 only.
assertEquals(wheelsStrokeColor.toArgb(), px[0]) // wheels: stroke untouched
assertEquals(Color(0xFF00FFFF).toArgb(), px[1]) // body: stroke recolored
assertEquals(Color(0xFF00FFFF).toArgb(), px[2]) // cab: stroke recolored
assertEquals(cargoStrokeColor.toArgb(), px[3]) // cargo: stroke untouched
}
}
Loading