В этой статье мы научимся создавать анимированный переключатель тем «луна-солнце» в Jetpack Compose с помощью Canvas.
Создание компонентов
Итоговые анимированные иконки должны состоять из трех основных компонентов: «луна-солнце» (moon-to-sun), «звезда» (star) и «лучи» (rays). Обсудим, как рисуются эти компоненты.
Компонент «луна-солнце»
Чтобы реализовать переход от формы луны к форме солнца, вычтем один круг из другого и отрегулируем расстояние между ними:
fun DrawScope.drawMoonToSun(radius: Float, progress: Float, color: Color) {
// Создание основного круга
val mainCircle = Path().apply {
addOval(Rect(center, radius))
}
// Вычисление начального положения вычитаемого круга
val initialOffset = center - Offset(radius * 2.3f, radius * 2.3f)
// Расчет смещения вычитаемого круга на основе прогресса
val offset = (radius * 1.8f) * progress
// Создание вычитаемого круга
val subtractCircle = Path().apply {
addOval(Rect(initialOffset + Offset(offset, offset), radius))
}
// Создание финального пути посредством вычитания вычитаемого круга из основного круга
val moonToSunPath = Path().apply {
op(mainCircle, subtractCircle, PathOperation.Difference)
}
// Отрисовка результирующего пути с указанным цветом
drawPath(moonToSunPath, color)
}
Компонент «лучи»
Для создания видимости лучей, исходящих из центральной точки (например, из солнца), эта функция рассчитывает углы и положения, чтобы равномерно распределить лучи по окружности:
private fun DrawScope.drawRays(
color: Color,
radius: Float,
rayWidth: Float,
rayLength: Float,
alpha: Float = 1f,
rayCount: Int = 8
) {
// Цикл для рисования каждого луча
for (i in 0 until rayCount) {
// Вычисление угла для текущего луча
val angle = (2 * Math.PI * i / rayCount).toFloat()
// Вычисление начального положения луча
val startX = center.x + radius * cos(angle)
val startY = center.y + radius * sin(angle)
// Вычисление конечного положения луча
val endX = center.x + (radius + rayLength) * cos(angle)
val endY = center.y + (radius + rayLength) * sin(angle)
// Проведение луча от начального до конечного положения
drawLine(
color = color,
alpha = alpha,
start = Offset(startX, startY),
end = Offset(endX, endY),
cap = StrokeCap.Round,
strokeWidth = rayWidth
)
}
}
Компонент «звезда»
Чтобы нарисовать форму звезды с центром в заданной точке и заданным радиусом, эта функция использует серию квадратичных кривых Безье:
private fun DrawScope.drawStar(
color: Color,
centerOffset: Offset,
radius: Float,
alpha: Float = 1f,
) {
val leverage = radius * 0.1f
val starPath = Path().apply {
// Переместитесь в самую левую точку звезды
moveTo(centerOffset.x - radius, centerOffset.y)
// Нарисуйте верхнюю левую кривую звезды
quadraticBezierTo(
x1 = centerOffset.x - leverage, y1 = centerOffset.y - leverage,
x2 = centerOffset.x, y2 = centerOffset.y - radius
)
// Нарисуйте верхнюю правую кривую звезды
quadraticBezierTo(
x1 = centerOffset.x + leverage, y1 = centerOffset.y - leverage,
x2 = centerOffset.x + radius, y2 = centerOffset.y
)
// Нарисуйте нижнюю правую кривую звезды
quadraticBezierTo(
x1 = centerOffset.x + leverage, y1 = centerOffset.y + leverage,
x2 = centerOffset.x, y2 = centerOffset.y + radius
)
// Нарисуйте нижнюю левую кривую звезды
quadraticBezierTo(
x1 = centerOffset.x - leverage, y1 = centerOffset.y + leverage,
x2 = centerOffset.x - radius, y2 = centerOffset.y
)
}
// Нарисуйте путь звезды
drawPath(starPath, color, alpha)
}
Главная функция
Теперь, когда у нас есть все компоненты, мы готовы определить главную функцию. Вот как она выглядит:
@Composable
fun MoonToSunSwitcher(
isMoon: Boolean,
color: Color,
modifier: Modifier = Modifier,
animationSpec: AnimationSpec<Float> = tween(400)
)
Параметры
isMoon
: определяет, должен ли переключатель отображать форму луны (true
) или форму солнца (false
);
color
: определяет цвет, который будет использоваться для рисования;
modifier
: определяет модификатор, который будет применяться к макету;
-
animationSpec
: определяет спецификацию анимации для перехода.
Реализация
В этом разделе мы соберем все вместе и создадим анимированную иконку. Посмотрим на код:
@Composable
fun MoonToSunSwitcher(
// Параметры...
) {
// Анимируйте прогресс на основе целевого значения
val progress by animateFloatAsState(
targetValue = if (isMoon) 1f else 0f,
animationSpec = animationSpec,
label = "Theme switcher progress"
)
Canvas(
modifier = modifier
.size(24.dp) // Установите размер Canvas по умолчанию
.aspectRatio(1f) // Убедитесь, что Canvas сохраняет соотношение сторон 1:1
) {
val width = size.width
val height = size.height
val baseRadius = width * 0.25f
val extraRadius = width * 0.2f * progress
val radius = baseRadius + extraRadius
// Поворачивайте Canvas в зависимости от прогресса
rotate(180f * (1 - progress)) {
// Вычислите прогресс рисования лучей
val raysProgress = if (progress < 0.5f) (progress / 0.85f) else 0f
// Нарисуйте лучи для формы солнца
drawRays(
color = color,
alpha = if (progress < 0.5f) 1f else 0f,
radius = (radius * 1.5f) * (1f - raysProgress),
rayWidth = radius * 0.3f,
rayLength = radius * 0.2f
)
// Нарисуйте переход от формы луны к форме солнца
drawMoonToSun(radius, progress, color)
}
// Рассчитайте прогресс для рисования звезд
val starProgress = if (progress > 0.8f) ((progress - 0.8f) / 0.2f) else 0f
// Нарисуйте звезды для луны
drawStar(
color = color,
centerOffset = Offset(width * 0.4f, height * 0.4f),
radius = (height * 0.05f) * starProgress,
alpha = starProgress
)
drawStar(
color = color,
centerOffset = Offset(width * 0.2f, height * 0.2f),
radius = (height * 0.1f) * starProgress,
alpha = starProgress
)
}
}
Мы успешно создали анимированную иконку. Полный код реализации вы можете найти на GitHub Gist. Теперь посмотрим, как можно применить это на практике.
Практическое использование
Определим переменную state, которая будет хранить текущее состояние иконки и менять его при нажатии:
var isMoon by remember { mutableStateOf(false) }
MoonToSunSwitcher(
modifier = Modifier.size(32.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { isMoon = !isMoon }
),
isMoon = isMoon,
color = Color.White
)
Вывод:
Читайте также:
- Создание снэкбара с обратным отсчетом времени в Android с помощью Jetpack Compose
- Как предотвратить утечки памяти в Android-приложении
- Jetpack DataStore: улучшенная система хранения данных
Читайте нас в Telegram, VK и Дзен
Перевод статьи Kappdev: How to Create an Animated Theme Switcher in Jetpack Compose