Композим иконки. Улучшаем семантику и скорость отрисовки

8be1e6d29aabe3530d89e040e8228306.png

Привет! Меня зовут Алексей, я работаю Android-разработчиком в Облаке Mail. Наша команда отвечает за возвращаемость пользователей в сервис. Чтобы сделать использование Облака приятным и удобным, мы проводим редизайн приложения, переписывая старый пользовательский интерфейс на Jetpack Compose по новым макетам. Для упрощения создания новых экранов мы разрабатываем UI Kit с готовыми Composable-компонентами.

Во время работы над новыми экранами мне часто приходилось использовать множество различных иконок, разбросанных по всему проекту. Это навело на мысль: было бы здорово собрать все иконки в UI Kit в единственном экземпляре и использовать их только оттуда — по аналогии с тем, как это делают дизайнеры в Figma. И тогда я вспомнил об одной особенности Jetpack Compose.

С момента появления в нём существует новый способ отрисовки иконок с помощью кода. На мой взгляд, этот метод отличается удобной семантикой и несколько более высокой производительностью. Чтобы наглядно продемонстрировать эти преимущества, давайте сначала рассмотрим, как работала отрисовка иконок раньше.

Как было раньше

С давних пор и до наших дней добавление иконок в проект Android осуществляется следующим образом: иконка в формате SVG преобразуется Android Studio в XML-ресурс, который затем используется в разметке пользовательского интерфейса.

Пример SVG иконки:


	

Пример получаемого ресурса XML:


  

Наиболее универсальным и распространённым способом описания иконок является path. Это набор команд, задающий координаты опорных точек и линий для отрисовки геометрических фигур любой сложности.

Вот основные команды path:

  • M/m (moveTo) — устанавливает опорную точку. Указываются координаты точки.

  • L/l (lineTo) — проводит линию от текущей точки до заданной. Указываются координаты заданной точки, которая становится новой опорной.

  • H/h (horizontalLineTo) — проводит горизонтальную линию от текущей точки до заданной. Указывается только координата x заданной точки. Координата y остаётся равной ординате текущей точки. Новая точка становится опорной.

  • V/v (verticalLineTo) — проводит вертикальную линию от текущей точки до заданной. Указывается только координата y заданной точки. Координата x остаётся равной абсциссе текущей точки. Новая точка становится опорной.

  • A/a (elliptical arc) — проводит эллиптическую дугу.

  • C/c, S/s (cubic Bezier curve) — проводит кубическую кривую Безье.

  • Q/q, T/t (quadratic Bezier curve) — проводит квадратичную кривую Безье.

  • Z/z (closePath) — завершает построение объекта Path, соединяя текущую точку с начальной. Не требует параметров.

Команды path бывают абсолютными и относительными. При использовании абсолютных команд все координаты задаются в системе координат рисунка, где начало отсчёта находится в верхнем левом углу.

При работе с большим количеством точек удобнее использовать относительные команды. Относительные команды обозначаются строчными буквами, а абсолютные — заглавными.

Пример простого треугольника в абсолютных координатах:


  

Результат

b2cfed1fad3db1fd5df97f6cfdcd8d29.png

Основная проблема работы с XML — отсутствие гибкости при создании UI. Это приводит к появлению в проекте множества вариантов одних и тех же иконок в разных размерах, цветах и с дополнительными контекстными элементами (например, тенью или подложкой, чтобы иконка выглядела как кнопка в тулбаре). В результате во многих проектах мы наблюдаем такую печальную картину в ресурсах:

c62d015277ff9585331c4262676ddc9a.png

Что изменилось в Compose

Jetpack Compose предлагает новый интересный способ создания иконок — с помощью кода. Это выглядит следующим образом:

_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()

Одно из преимуществ данного метода заключается в том, что при отрисовке иконки нам не нужно парсить XML и path — всё уже записано в виде функций, повторяющих команды из path. Это даёт небольшое ускорение — в статье приводится сравнение производительности двух подходов.

Но самое главное улучшение, на мой взгляд, — это существенное повышение семантики кода с иконками. С обычными ресурсами нам порой приходится писать длинные и не очень понятные идентификаторы.

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

В то же время с Compose иконками можно работать как с обычными классами. UI на Compose сам по себе очень гибкий, и в нём нет необходимости создавать копии одной и той же иконки. Достаточно просто добавить иконку в том виде, в котором она есть в дизайн-системе, а дальше её размеры и цвет можно легко изменять параметрами функции Icon. Благодаря гибкости и абстракциям в Compose-коде можно легко и наглядно создавать UI.

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

Подход с классами также позволяет легко разделять различные наборы иконок, если вы используете несколько разных наборов (например, VKUI и Paradigm).

Как добавлять иконки

К сожалению, в настоящее время Android Studio не имеет встроенного конвертера для преобразования иконок в Compose-код. Для этой цели необходимо использовать сторонние инструменты. В моём случае наиболее удобными оказались следующие два:

Эти два инструмента имеют некоторые отличия в работе:

  1. Сайт включает в генерируемый код все аргументы функции path, даже те, что имеют значения по умолчанию. Плагин же добавляет только аргументы, отличающиеся от стандартных, что делает код более компактным.

  2. Сайт не округляет координаты иконок, в то время как плагин округляет их до наименьшего значимого знака после запятой, используя внутренние оптимизации Android Studio (см. issue). Эта оптимизация применяется даже при добавлении иконок в формате XML (ссылка).

Пример работы конвертера сайта:

_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()

Пример работы конвертера плагина:

_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()

При планировании работы с иконками стоит изначально выбрать один из этих двух инструментов и использовать только его. На мой взгляд, плагин лучше подходит для этой задачи — в своём проекте я добавлял иконки именно через него.

Надеюсь, этот материал оказался для вас полезным. Спасибо за внимание!

Источники

  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

© Habrahabr.ru