![](https://habrastorage.org/getpro/habr/upload_files/8be/1e6/d29/8be1e6d29aabe3530d89e040e8228306.png)
Привет! Меня зовут Алексей, я работаю Android-разработчиком в Облаке Mail. Наша команда отвечает за возвращаемость пользователей в сервис. Чтобы сделать использование Облака приятным и удобным, мы проводим редизайн приложения, переписывая старый пользовательский интерфейс на Jetpack Compose по новым макетам. Для упрощения создания новых экранов мы разрабатываем UI Kit с готовыми Composable-компонентами.
Во время работы над новыми экранами мне часто приходилось использовать множество различных иконок, разбросанных по всему проекту. Это навело на мысль: было бы здорово собрать все иконки в UI Kit в единственном экземпляре и использовать их только оттуда — по аналогии с тем, как это делают дизайнеры в Figma. И тогда я вспомнил об одной особенности Jetpack Compose.
С момента появления в нём существует новый способ отрисовки иконок с помощью кода. На мой взгляд, этот метод отличается удобной семантикой и несколько более высокой производительностью. Чтобы наглядно продемонстрировать эти преимущества, давайте сначала рассмотрим, как работала отрисовка иконок раньше.
Как было раньше
С давних пор и до наших дней добавление иконок в проект Android осуществляется следующим образом: иконка в формате SVG преобразуется Android Studio в XML-ресурс, который затем используется в разметке пользовательского интерфейса.
Пример SVG иконки:
<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>
Пример получаемого ресурса XML:
<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>
Наиболее универсальным и распространённым способом описания иконок является 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 бывают абсолютными и относительными. При использовании абсолютных команд все координаты задаются в системе координат рисунка, где начало отсчёта находится в верхнем левом углу.
При работе с большим количеством точек удобнее использовать относительные команды. Относительные команды обозначаются строчными буквами, а абсолютные — заглавными.
Пример простого треугольника в абсолютных координатах:
<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>
Результат
![](https://habrastorage.org/getpro/habr/upload_files/b2c/fed/1fa/b2cfed1fad3db1fd5df97f6cfdcd8d29.png)
Основная проблема работы с XML — отсутствие гибкости при создании UI. Это приводит к появлению в проекте множества вариантов одних и тех же иконок в разных размерах, цветах и с дополнительными контекстными элементами (например, тенью или подложкой, чтобы иконка выглядела как кнопка в тулбаре). В результате во многих проектах мы наблюдаем такую печальную картину в ресурсах:
![](https://habrastorage.org/getpro/habr/upload_files/c62/d01/527/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-код. Для этой цели необходимо использовать сторонние инструменты. В моём случае наиболее удобными оказались следующие два:
Сайт https://www.composables.com/svgtocompose. Это ресурс с документацией по Compose Multiplatform, который также предоставляет конвертер иконок.
Плагин для Android Studio https://github.com/ComposeGears/Valkyrie. Он позволяет добавлять иконки непосредственно в IDE.
Эти два инструмента имеют некоторые отличия в работе:
Сайт включает в генерируемый код все аргументы функции path, даже те, что имеют значения по умолчанию. Плагин же добавляет только аргументы, отличающиеся от стандартных, что делает код более компактным.
Сайт не округляет координаты иконок, в то время как плагин округляет их до наименьшего значимого знака после запятой, используя внутренние оптимизации 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()
При планировании работы с иконками стоит изначально выбрать один из этих двух инструментов и использовать только его. На мой взгляд, плагин лучше подходит для этой задачи — в своём проекте я добавлял иконки именно через него.
Надеюсь, этот материал оказался для вас полезным. Спасибо за внимание!