Composing icons. Improving semantics and rendering speed

Hi! My name is Alexey, I work as an Android developer at Cloud Mail. Our team is responsible for user retention. To make using Cloud pleasant and convenient, we are redesigning the app, rewriting the old user interface on Jetpack Compose using new layouts. To simplify the creation of new screens, we are developing a UI Kit with ready-made Composable components.

When working on new screens, I often had to use a lot of different icons scattered throughout the project. This gave me an idea: it would be great to collect all the icons in a single UI Kit and use them only from there – similar to how designers do it in Figma. And then I remembered one feature of Jetpack Compose.

Since its introduction, there has been a new way to draw icons using code. In my opinion, this method has convenient semantics and slightly higher performance. To clearly demonstrate these advantages, let's first look at how icon drawing worked before.

How it was before

From ancient times to the present day, adding icons to an Android project is done as follows: an SVG icon is converted by Android Studio into an XML resource, which is then used in the UI markup.

Example of SVG icon:

<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
	<path d="M19.5 19.5V28.5C19.5 29.3284 18.8284 30 18 30C17.1716 30 16.5 29.3284 16.5 28.5V19.5H7.5C6.67157 19.5 6 18.8284 6 18C6 17.1716 6.67157 16.5 7.5 16.5H16.5V7.5C16.5 6.67157 17.1716 6 18 6C18.8284 6 19.5 6.67157 19.5 7.5V16.5H28.5C29.3284 16.5 30 17.1716 30 18C30 18.8284 29.3284 19.5 28.5 19.5H19.5Z" fill="#99A2AD"/>
</svg>

Example of the resulting XML resource:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="36dp"
    android:height="36dp"
    android:viewportWidth="36"
    android:viewportHeight="36">
  <path
      android:pathData="M19.5,19.5V28.5C19.5,29.328 18.828,30 18,30C17.172,30 16.5,29.328 16.5,28.5V19.5H7.5C6.672,19.5 6,18.828 6,18C6,17.172 6.672,16.5 7.5,16.5H16.5V7.5C16.5,6.672 17.172,6 18,6C18.828,6 19.5,6.672 19.5,7.5V16.5H28.5C29.328,16.5 30,17.172 30,18C30,18.828 29.328,19.5 28.5,19.5H19.5Z"
      android:fillColor="#99A2AD"/>
</vector>

The most universal and common way to describe icons is path. This is a set of commands that specifies the coordinates of reference points and lines for drawing geometric figures of any complexity.

Here are the basic path commands:

  • M/m (moveTo) — sets the reference point. The coordinates of the point are specified.

  • L/l (lineTo) — draws a line from the current point to the specified one. The coordinates of the specified point are specified, which becomes the new reference point.

  • H/h (horizontalLineTo) — draws a horizontal line from the current point to the specified one. Only the coordinate is specified x given point. Coordinate y remains equal to the ordinate of the current point. The new point becomes the reference point.

  • V/v (verticalLineTo) — draws a vertical line from the current point to the specified one. Only the coordinate is specified y given point. Coordinate x remains equal to the abscissa of the current point. The new point becomes the reference point.

  • A/a (elliptical arc) – draws an elliptical arc.

  • C/c, S/s (cubic Bezier curve) — draws a cubic Bezier curve.

  • Q/q, T/t (quadratic Bezier curve) — draws a quadratic Bezier curve.

  • Z/z (closePath) — completes the construction of the Path object, connecting the current point to the starting point. Requires no parameters.

Path commands can be absolute or relative. When using absolute commands, all coordinates are specified in the drawing coordinate system, where the origin is in the upper left corner.

When working with a large number of points, it is more convenient to use relative commands. Relative commands are denoted by lowercase letters, and absolute commands are denoted by uppercase letters.

Example of a simple triangle in absolute coordinates:

<svg width="200" height="180">
  <path stroke="orange" stroke-width="10" fill="gold"
    d="M 100,20 L 180,160
       L 20,160 z"/>
</svg>

Result

The main problem with working with XML is the lack of flexibility when creating UI. This leads to the appearance in the project of many variants of the same icons in different sizes, colors and with additional contextual elements (for example, a shadow or a background, so that the icon looks like a button in the toolbar). As a result, in many projects we see such a sad picture in the resources:

What's changed in Compose

Jetpack Compose offers a new and interesting way to create icons – using code. It looks like this:

_arrowLeft20 = ImageVector.Builder(
    name = "ArrowLeft20",
    defaultWidth = 20.dp,
    defaultHeight = 20.dp,
    viewportWidth = 20f,
    viewportHeight = 20f
).apply {
    path(fill = SolidColor(Color(0xFF99A2AD))) {
        moveTo(8.707f, 4.233f)
        curveTo(8.993f, 3.933f, 9.467f, 3.921f, 9.767f, 4.207f)
        curveTo(10.067f, 4.493f, 10.079f, 4.967f, 9.793f, 5.267f)
        lineTo(6f, 9.25f)
        horizontalLineTo(15.75f)
        curveTo(16.164f, 9.25f, 16.5f, 9.586f, 16.5f, 10f)
        curveTo(16.5f, 10.414f, 16.164f, 10.75f, 15.75f, 10.75f)
        horizontalLineTo(6f)
        lineTo(9.793f, 14.733f)
        curveTo(10.079f, 15.033f, 10.067f, 15.507f, 9.767f, 15.793f)
        curveTo(9.467f, 16.079f, 8.993f, 16.067f, 8.707f, 15.767f)
        lineTo(3.707f, 10.517f)
        curveTo(3.431f, 10.228f, 3.431f, 9.772f, 3.707f, 9.483f)
        lineTo(8.707f, 4.233f)
        close()
    }
}.build()

One of the advantages of this method is that when drawing an icon, we do not need to parse XML and path – everything is already written as functions that repeat commands from path. This gives a slight speedup – in article A comparison of the performance of the two approaches is provided.

But the most important improvement, in my opinion, is a significant increase in the semantics of the code with icons. With regular resources, we sometimes have to write long and not very clear identifiers.

(activity as AppCompatActivity)
    .supportActionBar
    ?.setHomeAsUpIndicator(ru.mail.cloud.uikitlibrary.R.drawable.ic_close_white)

At the same time, you can work with Compose icons as with regular classes. The UI on Compose is very flexible in itself, and there is no need to create copies of the same icon. It is enough to simply add the icon in the form in which it is in the design system, and then its size and color can be easily changed using the parameters of the Icon function. Thanks to the flexibility and abstractions in Compose code, you can easily and clearly create a UI.

CloudToolbar(
    leftContent = {
        CloudNavbarButton(
            icon = VkUiIcons.Outlined.Cancel20,
            onClick = { onBackPress() },
        )
    },
)

The class approach also makes it easy to separate different icon sets if you're using multiple different sets (like VKUI and Paradigm).

How to add icons

Unfortunately, Android Studio currently does not have a built-in converter for converting icons to Compose code. For this purpose, you need to use third-party tools. In my case, the following two turned out to be the most convenient:

These two tools have some differences in operation:

  1. The site includes all arguments of the path function in the generated code, even those with default values. The plugin only adds arguments that differ from the standard ones, which makes the code more compact.

  2. The site does not round icon coordinates, while the plugin rounds them to the least significant decimal place using Android Studio's internal optimizations (see issue). This optimization applies even when adding icons in XML format (link).

Example of the website converter in action:

_Add36 = ImageVector.Builder(
    name = "Add36",
    defaultWidth = 36.dp,
    defaultHeight = 36.dp,
    viewportWidth = 36f,
    viewportHeight = 36f
).apply {
	path(
		fill = SolidColor(Color(0xFF99A2AD)),
		fillAlpha = 1.0f,
		stroke = null,
		strokeAlpha = 1.0f,
		strokeLineWidth = 1.0f,
		strokeLineCap = StrokeCap.Butt,
		strokeLineJoin = StrokeJoin.Miter,
		strokeLineMiter = 1.0f,
		pathFillType = PathFillType.NonZero
	) {
		moveTo(19.5f, 19.5f)
		verticalLineTo(28.5f)
		curveTo(19.5f, 29.3284f, 18.8284f, 30f, 18f, 30f)
		curveTo(17.1716f, 30f, 16.5f, 29.3284f, 16.5f, 28.5f)
		verticalLineTo(19.5f)
		horizontalLineTo(7.5f)
		curveTo(6.6716f, 19.5f, 6f, 18.8284f, 6f, 18f)
		curveTo(6f, 17.1716f, 6.6716f, 16.5f, 7.5f, 16.5f)
		horizontalLineTo(16.5f)
		verticalLineTo(7.5f)
		curveTo(16.5f, 6.6716f, 17.1716f, 6f, 18f, 6f)
		curveTo(18.8284f, 6f, 19.5f, 6.6716f, 19.5f, 7.5f)
		verticalLineTo(16.5f)
		horizontalLineTo(28.5f)
		curveTo(29.3284f, 16.5f, 30f, 17.1716f, 30f, 18f)
		curveTo(30f, 18.8284f, 29.3284f, 19.5f, 28.5f, 19.5f)
		horizontalLineTo(19.5f)
		close()
	}
}.build()

Example of the plugin converter working:

_Add36 = ImageVector.Builder(
    name = "Add36",
    defaultWidth = 36.dp,
    defaultHeight = 36.dp,
    viewportWidth = 36f,
    viewportHeight = 36f
).apply {
    path(fill = SolidColor(Color(0xFF99A2AD))) {
        moveTo(19.5f, 19.5f)
        verticalLineTo(28.5f)
        curveTo(19.5f, 29.328f, 18.828f, 30f, 18f, 30f)
        curveTo(17.172f, 30f, 16.5f, 29.328f, 16.5f, 28.5f)
        verticalLineTo(19.5f)
        horizontalLineTo(7.5f)
        curveTo(6.672f, 19.5f, 6f, 18.828f, 6f, 18f)
        curveTo(6f, 17.172f, 6.672f, 16.5f, 7.5f, 16.5f)
        horizontalLineTo(16.5f)
        verticalLineTo(7.5f)
        curveTo(16.5f, 6.672f, 17.172f, 6f, 18f, 6f)
        curveTo(18.828f, 6f, 19.5f, 6.672f, 19.5f, 7.5f)
        verticalLineTo(16.5f)
        horizontalLineTo(28.5f)
        curveTo(29.328f, 16.5f, 30f, 17.172f, 30f, 18f)
        curveTo(30f, 18.828f, 29.328f, 19.5f, 28.5f, 19.5f)
        horizontalLineTo(19.5f)
        close()
    }
}.build()

When planning work with icons, it is worth initially choosing one of these two tools and using only it. In my opinion, the plugin is better suited for this task – in my project, I added icons through it.

I hope this material was useful for you. Thank you for your attention!

Sources

  1. https://webmaster.alexanderklimov.ru/html/svg/path.php

  2. http://css.yoksel.ru/svg-path/

  3. https://medium.com/@farbod.bijary/imagevector-vs-xml-drawable-a-performance-guide-fbad5135bbe8

  4. https://www.composables.com/svgtocompose

  5. https://github.com/ComposeGears/Valkyrie

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *