Files
amethyst/.claude/skills/compose-expert/references/icon-assets.md
T
2025-12-30 15:45:02 +02:00

9.5 KiB
Raw Blame History

Custom Icon Assets and ImageVector Patterns

Guide to creating and using custom ImageVector icons in Compose Multiplatform.

Why ImageVector?

ImageVector is the native Compose format for vector graphics:

  • Pure Kotlin: No XML, no asset files
  • Multiplatform: Works on Android, Desktop, iOS without conversion
  • Performant: Lightweight, composable, GPU-accelerated
  • Type-safe: Compile-time checking, no resource IDs

Amethyst Pattern: Robohash

Amethyst generates deterministic avatars using ImageVector builders.

Architecture

commons/robohash/
├── RobohashAssembler.kt    # Main assembly logic
├── CachedRobohash.kt        # Caching layer
└── parts/
    ├── Face0C3po.kt         # Face variants (0-9)
    ├── Eyes2Single.kt       # Eye variants (0-9)
    ├── Mouth3Grid.kt        # Mouth variants (0-9)
    ├── Body2Thinnest.kt     # Body variants (0-9)
    └── Accessory7Antenna.kt # Accessory variants (0-9)

Pattern: 10 variants per feature × 5 features = 100,000+ unique combinations

roboBuilder DSL

Custom ImageVector builder with sensible defaults:

fun roboBuilder(block: Builder.() -> Unit): ImageVector {
    return ImageVector.Builder(
        name = "Robohash",
        defaultWidth = 300.dp,
        defaultHeight = 300.dp,
        viewportWidth = 300f,
        viewportHeight = 300f
    ).apply(block).build()
}

Usage:

@Composable
fun CustomIcon() {
    Image(
        painter = rememberVectorPainter(
            roboBuilder {
                // Add paths here
            }
        ),
        contentDescription = "Custom icon"
    )
}

Path Building Pattern

fun face0C3po(fgColor: SolidColor, builder: Builder) {
    builder.addPath(pathData1, fill = fgColor, stroke = Black, strokeLineWidth = 1.5f)
    builder.addPath(pathData2, fill = Black, fillAlpha = 0.4f)
    builder.addPath(pathData5, fill = Black, fillAlpha = 0.2f)
    builder.addPath(pathData6, stroke = Black, strokeLineWidth = 1.0f)
    builder.addPath(pathData7, fill = Black, stroke = Black, fillAlpha = 0.2f, strokeLineWidth = 0.75f)
}

private val pathData1 = PathData {
    moveTo(144.5f, 87.5f)
    reflectiveCurveToRelative(-51.0f, 3.0f, -53.0f, 55.0f)
    curveToRelative(0.0f, 0.0f, 0.0f, 27.0f, 5.0f, 42.0f)
    reflectiveCurveToRelative(10.0f, 38.0f, 10.0f, 38.0f)
    lineToRelative(16.0f, 16.0f)
    // ...
    close()
}

Key elements:

  • pathData variables for path commands
  • addPath() for each layer
  • Parameterized colors (fgColor)
  • Constant colors (Black)
  • Alpha for shadows/highlights

PathData DSL

Compose's PathData builder provides SVG-like commands:

Command Description Example
moveTo(x, y) Move pen without drawing moveTo(100f, 100f)
lineTo(x, y) Draw line to point lineTo(200f, 150f)
curveToRelative(...) Relative cubic Bézier curveToRelative(10f, 20f, 30f, 40f, 50f, 60f)
reflectiveCurveToRelative(...) Smooth curve reflectiveCurveToRelative(-51f, 3f, -53f, 55f)
horizontalLineTo(x) Horizontal line horizontalLineTo(250f)
verticalLineTo(y) Vertical line verticalLineTo(300f)
close() Close path close()

Relative vs Absolute:

  • moveTo / lineTo - Absolute coordinates
  • moveToRelative / lineToRelative - Relative to current position

Creating Custom Icons

  1. Export SVG from design tool (Figma, Illustrator)
  2. Convert to ImageVector using Android Studio's Vector Asset tool
  3. Extract path data and adapt to roboBuilder pattern
// SVG path: M 10 10 L 20 20 ...
// Becomes:
private val myIconPath = PathData {
    moveTo(10f, 10f)
    lineTo(20f, 20f)
    // ...
}

Method 2: Programmatic

Build paths programmatically for simple shapes:

fun simpleIcon(): ImageVector = roboBuilder {
    addPath(
        pathData = PathData {
            moveTo(50f, 50f)
            lineTo(150f, 50f)
            lineTo(150f, 150f)
            lineTo(50f, 150f)
            close()
        },
        fill = SolidColor(Color.Blue),
        stroke = SolidColor(Color.Black),
        strokeLineWidth = 2f
    )
}

Method 3: Material Icons Extensions

Extend Material Icons when you need platform-consistent icons:

// For standard icons, use Material Icons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*

Icon(Icons.Default.Refresh, contentDescription = "Refresh")
Icon(Icons.Default.Check, contentDescription = "Success")
Icon(Icons.Default.Close, contentDescription = "Error")

CachedRobohash Pattern

Performance optimization for generated icons:

object CachedRobohash {
    private val cache = mutableMapOf<Pair<String, Boolean>, ImageVector>()

    fun get(seed: String, isLight: Boolean): ImageVector {
        return cache.getOrPut(seed to isLight) {
            RobohashAssembler.assemble(seed, isLight)
        }
    }
}

Pattern:

  • Key: (seed, theme) pair
  • Value: Assembled ImageVector
  • Lifecycle: Application lifetime (never cleared)

Usage:

@Composable
fun RobohashImage(robot: String) {
    Image(
        imageVector = CachedRobohash.get(robot, isLightTheme()),
        contentDescription = "Avatar for $robot"
    )
}

Color Management

Dynamic Colors

Pass colors as parameters for theme adaptation:

fun themedIcon(fgColor: SolidColor, bgColor: SolidColor, builder: Builder) {
    builder.addPath(pathData1, fill = bgColor)
    builder.addPath(pathData2, fill = fgColor)
}

@Composable
fun ThemedIcon() {
    val fg = MaterialTheme.colorScheme.primary
    val bg = MaterialTheme.colorScheme.surface

    Image(
        painter = rememberVectorPainter(
            roboBuilder {
                themedIcon(SolidColor(fg), SolidColor(bg), this)
            }
        ),
        contentDescription = null
    )
}

Static Colors

Define constants for colors that don't change:

val Black = SolidColor(Color.Black)
val White = SolidColor(Color.White)
val Transparent = SolidColor(Color.Transparent)

Advanced Techniques

Layering

Build complex icons with multiple layers:

fun complexIcon(builder: Builder) {
    // Layer 1: Background
    builder.addPath(bgPath, fill = SolidColor(Color.White))

    // Layer 2: Shadow
    builder.addPath(shadowPath, fill = SolidColor(Color.Black), fillAlpha = 0.2f)

    // Layer 3: Main shape
    builder.addPath(mainPath, fill = SolidColor(Color.Blue))

    // Layer 4: Highlight
    builder.addPath(highlightPath, fill = SolidColor(Color.White), fillAlpha = 0.3f)

    // Layer 5: Stroke
    builder.addPath(outlinePath, stroke = SolidColor(Color.Black), strokeLineWidth = 1f)
}

Render order: Bottom to top (first addPath = bottom layer)

Alpha for Visual Effects

// Shadow
builder.addPath(shadowPath, fill = Black, fillAlpha = 0.4f)

// Highlight
builder.addPath(highlightPath, fill = White, fillAlpha = 0.2f)

// Glass effect
builder.addPath(glassPath, fill = White, fillAlpha = 0.1f)

Stroke Styles

// Outline only
builder.addPath(path, stroke = Black, strokeLineWidth = 1.5f)

// Fill + outline
builder.addPath(path, fill = fgColor, stroke = Black, strokeLineWidth = 1f)

// Dashed (not supported directly, use multiple segments)

Composable Icon Pattern

Wrap ImageVector in a Composable for reusability:

@Composable
fun MyCustomIcon(
    modifier: Modifier = Modifier,
    tint: Color = Color.Unspecified
) {
    Image(
        painter = rememberVectorPainter(myIconVector()),
        contentDescription = "My custom icon",
        modifier = modifier,
        colorFilter = if (tint != Color.Unspecified) {
            ColorFilter.tint(tint)
        } else null
    )
}

Usage:

MyCustomIcon(
    modifier = Modifier.size(24.dp),
    tint = MaterialTheme.colorScheme.primary
)

Best Practices

DO

Cache generated ImageVectors for performance Use PathData DSL for readability Parameterize colors for theme support Use Material Icons for standard icons Keep viewport size consistent (e.g., 300×300) Layer paths from back to front Use alpha for shadows and highlights

DON'T

Generate ImageVectors in @Composable without caching Hardcode theme-specific colors Create custom icons for standard Material icons Use extreme viewport sizes (stay 24-1000dp) Mix absolute and relative coordinates unnecessarily Forget to close() paths

Icon Organization

Structure

commons/icons/
├── CustomIcons.kt           # Icon collection object
├── icons/
│   ├── Zap.kt              # Lightning bolt
│   ├── Relay.kt            # Relay indicator
│   └── Bitcoin.kt          # Bitcoin symbol
└── builders/
    └── IconBuilder.kt       # Shared builder utilities

Collection Object

object CustomIcons {
    val Zap: ImageVector by lazy { ZapIcon.create() }
    val Relay: ImageVector by lazy { RelayIcon.create() }
    val Bitcoin: ImageVector by lazy { BitcoinIcon.create() }
}

// Usage
Icon(CustomIcons.Zap, contentDescription = "Zap")

Resources