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

В этой статье мы разработаем компонент Custom Circle Loader (кастомизированный круговой загрузчик) с помощью Jetpack Compose. Присоединяйтесь, чтобы открыть для себя новые возможности.

Настройка

Начнем с создания класса данных StrokeStyle, необходимого для работы со стилем обводки загрузчика.

data class StrokeStyle(
val width: Dp = 4.dp,
val strokeCap: StrokeCap = StrokeCap.Round,
val glowRadius: Dp? = 4.dp
)

Создание функции

Теперь определим и реализуем composable-функцию CircleLoader.

Сигнатура функции

@Composable
fun CircleLoader(
modifier: Modifier,
isVisible: Boolean,
color: Color,
secondColor: Color? = color,
tailLength: Float = 140f,
smoothTransition: Boolean = true,
strokeStyle: StrokeStyle = StrokeStyle(),
cycleDuration: Int = 1400,
)

Объяснение параметров

  • modifier ➜ модификатор, применяемый к composable-компоненту Canvas и позволяющий настраивать макет и внешний вид;
  • isVisible ➜ определяет видимость анимации загрузчика;
  • color ➜ определяет основной цвет загрузчика;
  • secondColor ➜ определяет опциональный дополнительный цвет загрузчика;
  • tailLength ➜ определяет угол поворота “хвоста” загрузчика, измеряемый в градусах;
  • smoothTransition ➜ определяет степень плавности перехода между состояниями видимости загрузчика;
  • strokeStyle ➜ определяет стиль обводки, используемой при рисовании загрузчика;
  • cycleDuration ➜ определяет длительность цикла анимации загрузчика, измеряемую в миллисекундах.

Реализация

После определения функции перейдем к этапу выполнения, чтобы реализовать анимацию.

Работа с объектом paint

Функция setupPaint настраивает объект paint, используемый для рисования. Он применяет стиль обводки и настройки кисти.

fun DrawScope.setupPaint(style: StrokeStyle, brush: Brush): Paint {
val paint = Paint().apply paint@{
[email protected] = true
[email protected] = PaintingStyle.Stroke
[email protected] = style.width.toPx()
[email protected] = style.strokeCap

brush.applyTo(size, this@paint, 1f)
}

style.glowRadius?.let { radius ->
paint.asFrameworkPaint().setShadowLayer(
/* радиус = */ radius.toPx(),
/* dx = */ 0f,
/* dy = */ 0f,
/* shadowColor = */ android.graphics.Color.WHITE
)
}

return paint
}

Магия анимации

Загрузчик имеет два типа анимации.

Анимация вращения (Rotation Animation): используется rememberInfiniteTransition для создания эффекта бесконечного вращения загрузчика.

val transition = rememberInfiniteTransition()
val spinAngel by transition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = cycleDuration,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
)
)

Анимация перехода состояния (State Transition Animation): используется Animatable для придания плавного перехода длине хвоста, имитируя эффект плавного появления и исчезновения; срабатывает при изменении состояния видимости и установке smoothTransition в true  —  в противном случае привязывает значение.

val tailToDisplay = remember { Animatable(0f) }

LaunchedEffect(isVisible) {
val targetTail = if (isVisible) tailLength else 0f
when {
smoothTransition -> smoothTransition -> tailToDisplay.animateTo(
targetValue = targetTail,
animationSpec = tween(cycleDuration, easing = LinearEasing)
)
else -> tailToDisplay.snapTo(targetTail)
}
}

Рисование в Canvas

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

Canvas(
modifier
// Применение анимации вращения
.rotate(spinAngel)
// Убедитесь, что CircleLoader сохраняет квадратное соотношение сторон
.aspectRatio(1f)
) {
// Итерация по ненулевым цветам
listOfNotNull(color, secondColor).forEachIndexed { index, color ->
// Если это не основной цвет, поворачиваем Canvas на 180 градусов.
rotate(if (index == 0) 0f else 180f) {
// Создание градиентной кисти для загрузчика
val brush = Brush.sweepGradient(
0f to Color.Transparent,
tailToDisplay.value / 360f to color,
1f to Color.Transparent
)
// Настройка объекта paint
val paint = setupPaint(strokeStyle, brush)

// Рисование хвоста загрузчика
drawIntoCanvas { canvas ->
canvas.drawArc(
rect = size.toRect(),
startAngle = 0f,
sweepAngle = tailToDisplay.value,
useCenter = false,
paint = paint
)
}
}
}
}

Поздравляю! Загрузчик успешно создан (полный код реализации можно найти на GitHub Gist). Осталось только научиться им пользоваться.

Использование

Манипулируя основным и дополнительным цветами, можно создать три вида анимации. Посмотрим, как этого добиться.

Во всех примерах будем использовать кнопку (button) и состояние (state) для переключения анимации:

var isLoading by remember { mutableStateOf(false) }

/* Здесь - код для CircleLoader ... */

Button(
onClick = { isLoading = !isLoading }
) {
Text(text = if (isLoading) "Stop" else "Start")
}

1. Одноцветный двойной хвост 

Чтобы получить этот эффект, нужно выставить основной цвет, а дополнительный по умолчанию будет таким же.

CircleLoader(
color = Color(0xFF1F79FF),
modifier = Modifier.size(100.dp),
isVisible = isLoading
)

Вывод:

2. Двойной хвост с разными цветами

Чтобы добиться такого эффекта, нужно указать дополнительный цвет.

CircleLoader(
color = Color(0xFF1F79FF),
secondColor = Color(0xFFFFE91F),
modifier = Modifier.size(100.dp),
isVisible = isLoading
)

Вывод:

3. Одинарный хвост

Для получения эффекта одинарного хвоста просто установите дополнительный цвет в null. Кроме того, при желании можно увеличить длину хвоста.

CircleLoader(
color = Color(0xFF1F79FF),
secondColor = null,
tailLength = 280f,
modifier = Modifier.size(100.dp),
isVisible = isLoading
)

Вывод:

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Kappdev: Building Custom Circle Loader in Jetpack Compose: Exploring Android Canvas and Animations

Предыдущая статьяЗагрузка файлов в хранилище Cloudflare R2: простое руководство
Следующая статьяСериализация с Kotlin Serialization