Создание кастомизированного кругового загрузчика в Jetpack Compose: изучение Android Canvas и анимации



Книга Создание кастомизированного кругового загрузчика в Jetpack Compose: изучение Android Canvas и анимации

В сфере разработки современных приложений плавная и визуально привлекательная анимация загрузки имеет решающее значение для пользовательского интерфейса. 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
)

Вывод:




205   0  

Comments

    Ничего не найдено.