Я всегда испытывала трудности с рисованием в Canvas, причем не только в Canvas с Compose, но и с любой другой технологией. Я понимала достаточно, чтобы выполнять необходимые задачи, но в то же время старался избегать работы с технологией.
В одно из воскресений мне захотелось заняться чем-то творческим. Мне также хотелось программировать, поэтому я начала с идеи создания загрузочного спиннера с помощью Canvas. Одно за другим, и я создала симпатичную анимацию, которая больше похожа на иллюстрацию, чем на загрузочный спиннер:
В этой и следующей статьях блога я расскажу о том, как я это сделала, и о некоторых вещах, которые я узнала на этом пути. Первая статья посвящена рисованию элементов на холсте, а следующая — анимации.
Идея
Первоначальная идея иллюстрации на космическую тематику возникла благодаря футболке, которая у меня есть. Это футболка I Need Space от компании Spark. Сначала я подумала, что могу попытаться воспроизвести ту же картинку, но когда я закончила с «Сатурном», то решила пойти дальше.
Я решила добавить другие элементы и сделать звезды по-другому. Я в восторге от того, что получилось! Давайте перейдем к созданию иллюстрации. Полный код для этой статьи вы можете найти в этом сниппете.
Компоненты
Прежде чем говорить о компонентах, я хочу отметить следующее: точные цифры, которые я здесь показываю, подходят для этого конкретного рисунка и по большей части не могут быть обобщены. Я рассчитала позиции и размеры на основе определенного холста, а не для холста с возможностью изменения размеров.
Первый компонент — это фон и сам холст. В качестве родительского компонента используется Column, а в качестве дочернего — Canvas. Это позволяет нам определить анимацию и другие свойства, которые нельзя определить в Canvas.
Для рисования фона мы используем модификатор drawBehind
в родительском Column, нарисуем круглый прямоугольник и зададим радиальную градиентную кисть с некоторыми цветами:
Modifier.drawBehind { drawRoundRect( size = size, cornerRadius = CornerRadius(44f), brush = Brush.radialGradient( colors = listOf( Color(0xFF02010a), Color(0xFF04052e), Color(0xFF140152), Color(0xFF22007c), Color.Transparent, ), radius = size.width * 0.75f, ), ) },
На этот момент рисунок выглядит следующим образом:
Сатурн
Первая планета, которую мы добавим — это Сатурн. Чтобы нарисовать его, нам понадобятся три элемента: контур планеты, пояс и линия внутри планеты. Давайте определим функцию расширения drawSaturn
, которая принимает смещение центра планеты и стиль контура и рисует контур:
fun DrawScope.drawSaturn( center: Offset, outlineStyle: Stroke, ) { drawCircle( center = center, color = Colors.white, radius = 100f, style = outlineStyle, ) }
Это круг, который использует координаты центра и стиль контура (который, кстати, представляет собой обводку шириной 4f). Мы также задаем радиус, в данном случае это жестко закодированное значение 100f.
Далее мы рисуем линию внутри планеты:
drawArc( topLeft = Offset( center.x - 80f, center.y - 80f ), color = Colors.white, startAngle = 180f, sweepAngle = 90f, useCenter = false, style = Stroke( width = 2f, ), size = Size(160f, 160f), )
Для этого мы используем функцию drawArc
. Она немного отличается от функции drawCircle
, которая принимает координаты центра и радиус. Для дуги нам нужно определить левую верхнюю координату для прямоугольной области вокруг дуги и размер дуги.
Угол startAngle
определяет угол начала рисования, а угол sweepAngle
— продолжительность рисования. Позиция 0 для углов находится в положении «три часа», поэтому мы хотим начать с угла 180f, а угол развертки — 90f, чтобы добиться нужного нам вида.
Мы также должны установить значение useCenter
в true
; в противном случае будет нарисована дополнительная линия через центр.
Последний элемент, завершающий Сатурн, — это пояс астероидов. Поскольку он не является полным кругом, мы снова используем drawArc
. Код выглядит следующим образом:
rotate(40f, center) { drawArc( color = Colors.white, startAngle = 217f, sweepAngle = 285f, useCenter = false, topLeft = Offset( center.x - 50f, center.y - 150f ), style = outlineStyle, size = Size(100f, 300f), ) }
Обратите внимание, что размер области, где нарисована дуга, не квадратный — из-за формы обода нам нужно использовать неквадратный размер, чтобы сделать ее больше похожей на эллипс, чем на круг.
На этом этапе рисунок выглядит следующим образом:
Планета
Следующий элемент, который нужно добавить — это еще одна планета. Я не хотела давать ей название и хотела сделать ее более общей, поэтому мы будем называть ее просто планетой.
Планета состоит из двух элементов: сама планета и луна, вращающаяся вокруг нее. Давайте начнем с контура планеты, который представляет собой круг:
drawCircle( center = center, color = Colors.white, radius = 80f, style = outlineStyle, )
Этот код прост: мы рисуем окружность в позиции, заданной центром-переменной, и с радиусом 80. Затем мы рисуем линии на планете. Для этого мы используем три пути, два из которых имеют эффект пунктира, а один — сплошная линия. Код для первой линии (самой верхней) выглядит следующим образом:
drawPath( path = Path().apply { moveTo(center.x - 82f, center.y) quadraticTo( center.x - 45f, center.y + 5f, center.x, center.y ) }, color = Colors.white, style = Stroke( width = 3f, pathEffect = PathEffect.dashPathEffect( floatArrayOf(60f, 10f, 50f, 10f), 0f ), ), )
Остальные похожи, только числа и эффекты путей немного отличаются. Ссылку на полный код вы можете найти в начале статьи.
Итак, мы рисуем путь от края круга к центру. Поскольку линия должна быть слегка изогнутой, мы используем quadraticTo
для достижения этого эффекта.
Другой компонент планеты — луна. Сначала луна рисуется в виде круга:
drawCircle( center = Offset( center.x - 100f, center.y + 80f ), color = Colors.white, radius = 15f, style = outlineStyle, )
А для линии, которая создает впечатление, что луна вращается вокруг планеты, мы используем drawArc
:
drawArc( topLeft = Offset(center.x - 125f, center.y - 125f), color = Colors.white, startAngle = 160f, sweepAngle = 200f, useCenter = false, style = Stroke( width = 2f, pathEffect = PathEffect.dashPathEffect( floatArrayOf(160f, 70f, 50f, 80f, 40f, 40f), 0f, ), ), size = Size(250f, 250f), )
В дуге нет ничего нового — она берет левую верхнюю координату относительно Луны. Начальный угол составляет 160 градусов, и далее она крутится на 200 градусов. Она не использует центр, чтобы избежать третьей линии, проходящей через центр, и имеет эффект dashedPathEffect
для придания внешнего вида.
На данный момент рисунок выглядит следующим образом:
Звезды
Последними элементами иллюстрации являются звезды. Давайте создадим функцию расширения для DrawScope
, чтобы нарисовать звезду, а затем используем ее для рисования всех звезд.
Функция принимает размер звезды, смещение от центра, цвет звезды и стиль контура. Давайте определим ее:
fun DrawScope.drawStar( starSize: Size, center: Offset, color: Color, outlineStyle: Stroke, ) { ... }
Мы используем путь с квадратичными кривыми Безье от одной начальной точки к другой. У звезды четыре точки, и если представить пространство для звезды в виде квадрата, то точки находятся в середине внешних контуров. Начнем с середины верхней стороны:
val path = Path() path.moveTo(center.x, center.y - starSize.height * 0.5f)
После этого мы хотим нарисовать квадратичную кривую Безье от одной точки к другой и использовать центр области звезды в качестве точки кривой. Для линии от верхней стороны до правой стороны это будет выглядеть следующим образом:
path.quadraticTo( center.x, center.y, center.x + starSize.width / 2, center.y, )
Первые два параметра — это координаты контрольной точки, а два последних — точка, в которой заканчивается линия. Остальные три линии выглядят так же — просто координаты конца меняются в зависимости от целевых координат.
После рисования этих четырех линий нам остается нарисовать путь:
drawPath( path = path, color = color, style = outlineStyle, )
Теперь, когда у нас есть функция drawStar
, мы можем нарисовать звезды. Мы хотим разместить их вокруг планет, и мы хотим, чтобы они были разного цвета. Нам также нужно хранить размер звезды. Давайте определим класс данных, который будет всем этим заниматься:
data class Star( val size: Float, val topLeft: Offset, val color: Color, )
Затем мы можем определить список звезд. Это будет выглядеть примерно так, с примером одной звезды в списке:
val starsList = listOf( Star( size = 30f, topLeft = Offset(size.width * 0.5f, size.height * 0.5f), color = Colors.stars[0], ), ... )
Мы можем задать разные размеры звезд и несколько разных цветов. Положение каждой звезды рассчитывается вручную.
Затем мы можем использовать список звезд и нарисовать их с помощью функции:
starsList.forEach { (starSize, offset, color) -> drawStar( starSize = Size(starSize, starSize), color = color, center = offset, outlineStyle = Stroke( width = 2f, ), ) }
После этих изменений иллюстрация выглядит следующим образом:
Итоги
В этой статье мы создали иллюстрацию с помощью Canvas. В настоящее время она статична, но в следующей статье мы анимируем элементы на иллюстрации.
Как вы оцениваете свои навыки работы с Canvas? Уверены ли вы в себе или избегаете этого?
Продолжение: Паря в космосе: анимации с помощью Compose и Canvas