foxtrotuniform6969
foxtrotuniform6969

Reputation: 4125

Jetpack Compose: Custom VectorAsset Icon object similar to built-in `Icons.Default`

It looks like the only way to go about loading custom icons from Android Vector Resources in the res folder is to do it within a @Composable function using the vectorResource(R.drawable.myVectorName) method.

This is great and all, but I like the syntax of fetching VectorAssets for the Icon(asset: VectorAsset) class, which looks like Icon(Icons.Default.Plus).

It looks like the vectorResource() method uses an internal method called loadVectorResource(), and the methods it uses to read the actual XML file composing the vector asset file are also internal.

How would I go about creating an object like MyAppIcons.Default.SomeIcon in Jetpack Compose?

EDIT

So, I have sort-of found a solution. However, it would be nice to make my own extension/overloading of the built-in Icon() function, but I'm not sure if there is a proper way to do this.

Upvotes: 19

Views: 24954

Answers (5)

LambergaR
LambergaR

Reputation: 2473

The approach that worked for me was adding a @Composable getter to the fields, like so:

object Icons {
    val Close: VectorIcon
       @Composable get(): ImageVector.vectorResource(R.drawable.close)
}

Those can then be used in way consistent with the rest of the compose framework, eg:

Icon(Icons.Close, contentDescription = "Close")

Upvotes: 2

machfour
machfour

Reputation: 2709

I went down the other route and extracted the logic from Jetpack Compose source code that turns an XML SVG path string into an ImageVector. In the end I came up with this:

fun makeIconFromXMLPath(
    pathStr: String,
    viewportWidth: Float = 24f,
    viewportHeight: Float = 24f,
    defaultWidth: Dp = 24.dp,
    defaultHeight: Dp = 24.dp,
    fillColor: Color = Color.White,
): ImageVector {
    val fillBrush = SolidColor(fillColor)
    val strokeBrush = SolidColor(fillColor)

    return ImageVector.Builder(
        defaultWidth = defaultWidth,
        defaultHeight = defaultHeight,
        viewportWidth = viewportWidth,
        viewportHeight = viewportHeight,
    ).run {
        addPath(
            pathData = addPathNodes(pathStr),
            name = "",
            fill = fillBrush,
            stroke = strokeBrush,
        )
        build()
    }
}

All you have to do is call this function with pathStr set to the value of android:pathData from the drawable XML file. Here's an example:

val AppleIcon by lazy { makeAppleIcon() }

// by Austin Andrews, found on https://materialdesignicons.com/
private fun makeAppleIcon(): ImageVector {
    return makeIconFromXMLPath(
        pathStr = "M20,10C22,13 17,22 15,22C13,22 13,21 12,21C11,21 11,22 9,22C7,22 2,13 4,10C6,7 9,7 11,8V5C5.38,8.07 4.11,3.78 4.11,3.78C4.11,3.78 6.77,0.19 11,5V3H13V8C15,7 18,7 20,10Z"
    )
}

@Preview
@Composable
fun AppleIconPreview() {
    Surface { 
        Icon(AppleIcon, "Apple")
    }
}

enter image description here

Upvotes: 4

yazan sayed
yazan sayed

Reputation: 1139

from Resources in Compose

Use the painterResource API to load either vector drawables or rasterized asset formats like PNGs. You don't need to know the type of the drawable, simply use painterResource in Image composables or paint modifiers.

// Files in res/drawable folders. For example:
// - res/drawable-nodpi/ic_logo.xml
// - res/drawable-xxhdpi/ic_logo.png

// In your Compose code
Icon(
    painter = painterResource(id = R.drawable.ic_logo),
    contentDescription = null // decorative element
)

Upvotes: 28

foxtrotuniform6969
foxtrotuniform6969

Reputation: 4125

Turns out I wasn't using my brain. The answer is pretty easy.

The gist is, Icon() is a composable function, meaning that of course vectorResource() can be used there.

So, the correct approach is no secret... it's to make your own MyAppIcon() component, call vectorResource() and then return a normal Icon(), like so:

Correct Way

@Composable
fun MyAppIcon(
    resourceId: Int,
    modifier: Modifier = Modifier,
    tint: Color = AmbientContentColor.current
) {
    Icon(
        asset = vectorResource(id = resourceId),
        modifier = modifier,
        tint = tint
    )
}

You can then create an object elsewhere, like so:

object MyAppIcons {
    val SomeIcon = R.drawable.someIcon
    val AnotherIcon = R.drawable.anotherIcon
}

When you put the two together, you can use it like this:

MyAppIcon(MyAppIcons.SomeIcon)

I'm hoping that Google just adds this override soon, allowing us to pass in resource IDs.

Upvotes: 18

Leha Bodunov
Leha Bodunov

Reputation: 63

There is a way to load asset using Icon(Icons.Default.Plus). You need to make an extesion property

val androidx.compose.material.icons.Icons.Filled.FiveG : VectorAsset
    get() {

    }

but I don't see the way to get VectorAsset outside of composable function. Of course you can do something like this

val androidx.compose.material.icons.Icons.Filled.FiveG : VectorAsset
    get() {
        return Assets.FiveG
    }

object Assets {
    lateinit var FiveG: VectorAsset
}

@Composable
fun initializeAssets() {
    Assets.FiveG = vectorResource(R.drawable.ic_baseline_5g_24)
}

but it's a bad idea to have a composable with side effect. So i'm waiting for someone to find a way to convert SVG to VectorAsset Kotlin class or get VectorAsset object outside of composable function.

Upvotes: 1

Related Questions